MODERN C++ SMART POINTERLAR

Modern C++ unique_ptr shared_ptr ve weak_ptr smart pointer ownership üç panel görselleştirme

"Smart pointer kullanıyorum, artık delete derdim yok" — C++ topluluğunda en sık duyulan ve en yarı doğru cümlelerden biri. Pek çok geliştirici unique_ptr ve shared_ptr'ı sihirli bir bellek temizleyicisi sanıyor, ama bu araçlar sadece kuralları kodlayan kaplardır. Sahiplik (ownership) modelini yanlış kurarsanız, smart pointer da sizi kurtarmaz: sızıntı yerine artık döngüsel referans, dangling iterator veya çifte serbest bırakma ile karşılaşırsınız. Bu yazıda yanılgıyı kırıp hangisi ne zaman sorusuna pratik bir cevap arıyoruz.

Yaygın Yanılgı: "Smart Pointer = Otomatik Bellek Yönetimi"

RAII güçlüdür ama büyü değildir. Smart pointer'lar, C++'ın stack-based deterministik yıkım davranışını heap nesnelerine taşır. Yani nesne, kendisini tutan akıllı işaretçi scope dışına çıktığında yok edilir. Buradaki kritik nokta: sahiplik kimde? sorusunu kod düzeyinde net cevaplamanız gerekir. Smart pointer bu cevabı sizin yerinize üretmez, sadece verdiğiniz cevabı uygular.

Sık karşılaşılan üç hatalı varsayım:

  • "shared_ptr her şeyi çözer." Hayır — cycle (döngüsel referans) varsa nesneler ölmez.
  • "unique_ptr kopyalanmıyorsa kullanışsız." Hayır — std::move ile transfer edilir, bu zaten istenen davranıştır.
  • "raw pointer artık tehlikeli." Hayır — sahiplik içermeyen erişim (observer) için raw pointer hâlâ geçerlidir.

unique_ptr: Tek Sahip, Sıfır Maliyet

std::unique_ptr<T>, bir nesne üzerinde tek bir sahip olduğunu garantiler. Kopyalanamaz, sadece taşınabilir. Çalışma zamanı maliyeti pratikte sıfırdır — raw pointer ile aynı boyuttadır (custom deleter yoksa).

Tipik kullanım:

  • Bir fonksiyonun yarattığı nesneyi çağırana tek elden devretmek: std::unique_ptr<Widget> create();
  • Sınıf üyesi olarak, bir kaynağın yaşam süresini sınıfın yaşam süresine kesin bağlamak.
  • Polimorfik konteynerler: std::vector<std::unique_ptr<Base>> — base pointer üzerinden virtual destructor ile doğru yıkım.

Sahiplik transferi açıkça std::move ile yapılır:

auto p = std::make_unique<Engine>(); car.attach(std::move(p)); // p artık nullptr

C++ unique_ptr tek sahip ve shared_ptr referans sayacı ownership transferi diyagramı

shared_ptr: Paylaşılan Sahiplik ve Gizli Maliyet

std::shared_ptr<T> birden fazla sahibin aynı nesneyi paylaşmasına izin verir. Arkasında bir control block vardır: strong count, weak count ve custom deleter saklanır. Son shared_ptr yok olunca nesne silinir.

Maliyet pratiktir ama görünmez değildir:

  • Reference count atomic olduğundan her kopya/yıkım atomik bir işlem demektir.
  • Pointer boyutu raw pointer'ın iki katıdır (nesne + control block adresi).
  • std::make_shared tercih edilmeli — nesne ve control block tek allocation'da oluşur.

shared_ptr'ı reflexte kullanmak yerine "gerçekten paylaşılan sahiplik var mı?" sorusunu sorun. Çoğu zaman cevap hayır'dır ve unique_ptr + raw observer yeterlidir.

Cycle Tuzağı: shared_ptr'ın Sessiz Sızıntısı

İki nesne birbirini shared_ptr ile tutarsa, reference count asla sıfıra inmez. Parent → child ilişkilerinde sıkça görülür:

struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; };

Burada prev weak_ptr olmalıdır. std::weak_ptr nesneye sahiplik almadan referans verir; erişmek için lock() ile geçici bir shared_ptr üretirsiniz. Bu, observer pattern'lerde ve cache'lerde de standart çözümdür.

Raw Pointer Hâlâ Yerinde: Non-Owning Access

Sahiplik içermeyen bir referansa ihtiyacınız varsa — bir fonksiyon parametresi olarak, kısa ömürlü bir gezinti için — raw pointer veya referans hâlâ doğru araçtır. Smart pointer'ı imza içine gömmek, çağıran fonksiyona yanlış bir sözleşme dayatmak demektir.

  • void process(Widget* w) — "ben sahibi değilim, sadece bakıyorum."
  • void process(std::unique_ptr<Widget> w) — "ben sahipliğini alıyorum."
  • void process(const std::shared_ptr<Widget>& w) — "paylaşımlı kullanıma katılıyorum."

İmzanız niyetinizi anlatır. C++ Core Guidelines'ın F.7 ve R.30 kuralları bu konuda nettir: smart pointer parametreleri sadece sahiplik semantiği gerçekten gerekliyse kullanılır. Standart kütüphanenin bellek yönetimi başlıklarına ilişkin ayrıntılı başvuru dokümantasyonu bu kuralların arkasındaki API ayrımlarını izlemek için iyi bir referanstır.

Karar Akışı: Hangisini Seçmeli?

Pratik bir öncelik sırası:

  1. Önce stack üzerinde value semantiği mümkün mü? Mümkünse smart pointer'a hiç gerek yok.
  2. Heap gerekli ve tek sahip varsa → unique_ptr.
  3. Birden çok sahip gerçekten varsa → shared_ptr.
  4. Geri referans veya observer varsa → weak_ptr ya da raw pointer.
  5. Polimorfik yıkım gerekiyorsa base sınıfta virtual destructor olduğundan emin olun.

C++'ın bu modern bellek araçlarını derinlemesine pratik etmek için Modern C++ eğitimi içeriklerinden yararlanabilirsiniz; ownership semantiği, move semantics ve RAII konuları orada bütünleşik anlatılır.

Performans Notları ve Sık Hatalar

Smart pointer hataları çoğunlukla derleme zamanında yakalanmaz; runtime'da, hatta production'da ortaya çıkar.

  • this'i shared_ptr'a sarmayın doğrudan: std::enable_shared_from_this kullanın.
  • Aynı raw pointer'ı iki ayrı shared_ptr ile sarmak çifte yıkım demektir.
  • get() ile elde ettiğiniz raw pointer'ı asla delete etmeyin — sahiplik smart pointer'da.
  • make_shared kullanıyorsanız nesnenin belleği control block ile aynı blokta tutulur; weak_ptr hâlâ yaşıyorken nesne yok olsa bile bu blok serbest kalmaz — büyük nesnelerde dikkat.
  • Multi-threaded ortamda aynı shared_ptr nesnesini farklı thread'lerden değiştirmek atomik değildir; atomic<shared_ptr<T>> (C++20) gerekir.
C++ shared_ptr döngüsel referans sorunu ve weak_ptr ile cycle kırılması şeması

Sonuç olarak smart pointer'lar bellek yönetimini otomatikleştirmez — sahiplik kararlarınızı kodlayan disiplinli araçlardır. Doğru aracı doğru semantikle seçtiğinizde C++ kodunuz hem güvenli hem deterministik kalır; yanlış seçildiğinde ise raw pointer döneminden daha sessiz, daha derin hatalar üretirler. unique_ptr'ı varsayılan, shared_ptr'ı istisna, weak_ptr'ı kurtarıcı olarak düşünmek pratik bir başlangıçtır.