Modern C++ (4) : Smart Pointers – II

Evet arkadaşlar akıllı işaretçiler serüvenimize devam ediyoruz. Bu yazımda akıllı C++11 ile gelen shared_ptr ve weak_ptr sınıflarını inceleyeceğiz. İlk yazıma aşağıdaki adresten ulaşabilirsiniz.

Modern C++ (4) : Smart Pointers – I

std::shared_ptr

Akıllı işaretçi denildiğinde akla gelen ilk sınıf std::shared_ptr. std::unique_ptr’dan farklı olarak “reference counting”  dediğimiz kabiliyeti barındıran, içerdiği nesnenin birden fazla sınıf tarafından kullanılmasına olanak sağlayan ve herhangi bir kullanıcı kalmadığında da ilgili sınıfı yok edebilen sınıftır kendisi. Aslında burada herhangi bir spesifik shared_ptr bu nesneye sahip değil ve herhangi bir bu nesnenin yok edilmesinden sorumlu değil.

Peki std::shared_ptr herhangi bir kullanıcı kalmadığını nereden anlıyor? İşte tam bu noktada “reference counting” mekanizması devreye giriyor. Bu mekanizmanın temelinde aslında std::shared_ptr akıllı işaretçisi içerisinde kaynağa olan referans adeti aracılığı ile tutulan nesneyi gösteren her bir işaretçi için tutulan sayıya dayanıyor.

  • std::shared_ptr constructor’ı çağrıldığında referans adeti bir arttırılır,
  • std::shared_ptr destructor’ı çağrıldığında ya da reset() API’si çağrıldığında (yani kapsamdan çıkınca veya benzeri durumlarda) referans adeti bir azaltılır,
  • std::shared_ptr copy constructor/assignment durumlarında ise (sharedPtrInstance1 = sharedPtrInstance2; örneği için) sharedPtrInstance1 tarafından yönetilen nesne referans adeti bir azaltılır (artık bu shared_ptr onu göstermiyor), sharedPtrInstance2 tarafından yönetilen nesne referans adeti bir arttırlır. Çünkü artık sharedPtrInstance1 de bu nesneyi kullanıyor.
  • Herhangi bir referans adeti azaltma işlemi sonrasında eğer bu adet 0 olur ise bu nesne otomatik olarak yok edilir,
  • std::shared_ptr’ların “move”  mekanizması ile taşınması durumunda ise referans adeti ile ilgili herhangi bir değişiklik olmaz ve bu sebeple de diğer işlemlere göre daha hızlı bir şekilde gerçekleştirilebilir.

Şimdi hızlıca std::unique_ptr’dan farklara ve fazlalıklara bakalım:

  • std::shared_ptr standart işaretcilere göre boyutu iki kata çıkartır. Yukarıda bahsettiğimiz işleri yapabilmek için std::shared_ptr orjinal nesneye bir standart işaretçi tutar ve bir de nesneye ilişkin referans adetine bir standart işaretçi tutulur (Not: C++ standardı bunu dikte etmiyor fakat genel olarak bu şekilde gerçekleniyor),
  • Nesneye olaran referans adeti dinamik olarak bellekten alınır (ve bir takım bir kaç bilgi daha. Kontrol bloğu başlığında buna değineceğiz),
  • Referans adeti arttırma ve azaltma işlemleri atomik olarak gerçekleştirilir, bu da normak olarak atomik olmayanlara göre çok çok az da olsa bir fazlalık getirir,
  • std::unique_ptr’larda olduğu gibi diziler için std::shared_ptr<T[]> tarzı bir kullanım sunulmamaktadır. Bunun yerine std::array, std::vector kullanımı değerlendirilmelidir,
  • unique_ptr’ı shared_ptr kullanımına dönüştürebilirsiniz ama tersi mümkün değil (zaten anlamlı da değil)

Genel Kullanım

shared_ptr kullanımına bakacak olursak. shared_ptr’ı new operatörü ile oluşturduğunuz bir nesneyi geçirerek oluşturabilirsiniz. Ayrıca C++ 14 ve sonrasında make_shared ya da bir önceki yazım da verdiğim template örneğini de kullanabilirsiniz.

Daha sonra bu shared_ptr nesnesini aynı tipte olan başka shared_ptr’lara atayabilir, kopyalayabilirsiniz, fonksiyondan dönebilirsiniz, konteynerler içerisine ekleyebilirsiniz. Burada dikkat etmeniz gereken shared_ptr’lar “copy by reference” mekanizması ile geçirmeniz durumunda bunu kullanacak metot için nesne adeti arttırılmaz. İlgili nesne kapsam dışında çıktığında, silindiğinde ya da reset() API’si çağrıldığında ilgili referans adeti azaltılır ve sıfır ise yönetilen nesne de silinir. Bu durumda elinizde boş bir shared_ptr nesnesi olur. Ayrıca herhangi bir shared_ptr nesnesine nullptr atayarak ta aynı sonucu alabilirsiniz.

Normalde çok kullanılması beklenmese de, ilgili shared_ptr nesnesinin işaret ettiği nesnenin standart işaretçisini almak için get() API’sini kullanabilirsiniz. Tabiki bunun ile yapacağınız işler için veya bu işaretçinin ilgili nesneti artık gösterip göstermediği konusunda herhangi bir garanti sunulmaz 🙂

==, != operatörleri aynen standart işaretçilerdeki gibi kullanılabilir. Bunlar altta yönetilen nesneye ilişkin işaretçileri karşılaştırır. if(sharedPtrInstance) kullanımı da eğer ilgili nesne boş ise false, eğer herhangi bir nesneye işaret ediyor ise true döner.

Aşağıda bütün bu kullanımlara değinen kod parçasını görebilirsiniz.

shared_ptr’da Miras ve “Casting” 

shared_ptr’larda da miraz benzer şekilde ele alınır. Örneğin standart işaretçiler için bulunan aşağıdaki hiyerarşiye bakalım:

Eğer shared_ptr tarafından yönetilen nesneler gerekli kopyalama kabiliyetlerini sunuyorlar ise aşağıdaki shared_ptr kullanımı gerçekleştirilebilmektedir:

Akıllı işaretçi nesnelerinin bir birlerine “cast” edilebilmesi için de standart işaretçiler için sunulan static_cast, dynamic_cast, const_cast muadilleri static_pointer_cast, dynamic_pointer_cast ve const_pointer_cast metotları sunulmuştur.

Özelleşmiş Silici (custom delete)

std::unique_ptr’da olduğu gibi std::shared_ptr de işaret ettiği kaynağı yok edilmesi için varsayılan olarak delete operatörünü kullanıyor, fakat önemli bir farklılık ta bulunmakta. O da, std::unique_ptr’da silicinin tipi akıllı işaretçinin parçası iken std::shared_ptr da kendisi bir parçası oluyor. Aşağıda buna ilişkin bir örnek görebilirsiniz. Bunun getirdiği en önemli fayda her bir nesneye farklı özelleşmiş silme mantıkları atayabilmek, bu std::unique_ptr’da mümkün değil.

Kontrol Bloğu

Burada şu akla gelebilir bütün bu özelleşmiş siliciler için shared_ptr ekstra bir bellek tutuyor mu? Bu sorunun cevabı hayır, bu shared_ptr’ın bir paraçacı değil. Tabi burda dikkat edilmesi gereken bir husus var. Bir kaç paragraf öncesinde ifade ettiğimiz gibi nesne referans adeti için ayrıca bir standart işaretçi tutuluyor ve bunun için bellekten yer alınıyor, fakat burada bu işaretçinin gösterdiği şey sadece nesne adeti değil ve daha büyük bir yapı aslında bu alanın/veri yapısı kontrol blok’u olarak ta adlandırılıyor. shared_ptr tarafından işaret edilen her bir nesne için kontrol bloğu tutuluyor. Bu blok, nesne referans adeti (Reference Count) yanında, eğer atanmışsa özelleşmiş silici biraz sonra anlatacağımız zayıf nesne referans adeti (Weak Count) ve yine atanmışsa özelleşmiş bellek yer alıcıları (Allocator). Aşağıdaki figürde bu anlatılan alanları görebilirsiniz.

Image

Bu kontrol bloğu ilk defa shared_ptr oluşturulduğu zaman veya benzeri durumlarda oluşturulmakta (std::make_shared çağrılarında, std::unique_ptr kullanılarak oluşturulduğunda, std::shared_ptr  standart bir işaretçi aracılığı ile oluşturulduğunda). Burada dikkat edilmesi gereken nokta elinizde bulunan standart işaretçiyi iki ayrı shared_ptr’a geçirirseniz iki ayrı kontrol bloğu oluşturursunuz. Bu sebeple shared_ptr’ları standart işaretçileri geçirerek oluşturmak yerine make_shared tarzı yapılar kullanmanız bu tür sıkıntıların önüne geçecektir. İlla standart işaretçiyi geçirmeniz gerekiyor ise new operatörü ile oluşturulmuş nesneyi geçirin. Aşağıda bu durumu özetleyen örnek kod parçaları eklemeye çalıştım.

std::enabled_shared_from_this

Yukarıda belirtilen kontrol bloğu ile ilgili benzer bir problemi de this kullanımında yaşayabilirsiniz. Örneğin aşağıdaki kod parçasında olduğu gibi std::vector konteynerı içerisinde shared_ptr<Node>’ları tutmak istediğimiz durumu düşünelim.

Normalde bu sınıfa dair yeni bir kontrol bloğu ile bir shared_ptr oluşturmak sıkıntı olarak görülmese de, bu sınıfın kendisinin de başka bir shared_ptr tarafından yönetilmesi durumunda sıkıntılar ortaya çıkacaktır (örneğin benzer şekilde iki kere emplace_back yaparsanız iki farklı kontrol bloğu oluşturulur). İşte tam da bu durumlar için C++ kütüphanesi std::enable_shared_from_this isim template bir temel sınıf sunar. Siz bu şekilde kullanmak istediğiniz sınıfları bu sınıftan türetirsiniz, bu sınıf ile birlikte shared_from_this() isimli bir API gelir. Yukarıdaki gibi shared_ptr oluşturma ihtiyacı olduğu durumlarda bu API’yi kullanabilirsiniz.  Bu da yukarıdaki durumlarda yaşanabilecek sıkıntıların (ekstra bir kontrol bloğu oluşturma vs’nin) önüne geçer. Aşağıda örnek kullanımı görebilirsiniz.

std::weak_ptr

Evet akıllı işaretçiler yazımızın son konuğu std::weak_ptr’lar. Bu akıllı işaretçiyi tek cümlede açıklamak gerekirse, “yönetilen nesneyi sadece gözlemleyip, var olup olmadığını kontrol edip, yaşam döngüsüne ilişkin herhangi bir mantık yürütmeyen veya müdahale etmeyen akıllı işaretçi” diyebiliriz. std::weak_ptr’lar shared_ptr gibi davranan fakat herhangi bir sahiplik göstermeyen (yani nesne adetleri üzerinde herhangi bir etkisi olmayan) akıllı işaretçi olarak ta tariflenebilir.

Peki neden ve ne için std::weak_ptr’ı kullanmalıyım? std::weak_ptr’lar aracılığı ile shared_ptr ile yönetilen bir nesnenin artık kullanılabilir olup olmadığını takip edebilirsiniz. Hemen bir örneğe bakalım:

Örneğin eğer kullanılabilir ise bu nesnenin sahipliğini almak isteyebilirsiniz. Bunun için de lock() API’si kullanılabilir. Eğer lock() null döner ise ilgili nesnenin yok edildiğini kabul edebilirsiniz.

Bir diğer weak_ptr kullanımı ise şu durumda ortaya çıkar. Şöyle bir senaryo düşünelim elimizde 3 sınıf var A, B ve C. Bunlarda A ve C, B’nin sahipliğini paylaşıyorlar ve B’den de örneğin A’ya bir referans tutulması ihtiyacı var ne kullanabiliriz?

Image

Image

Üç seçeneğimiz var:

  1. Standart işaretçi: Bu durumda eğer A yok edilir ve C halen B’yi tuttuğu için aslında yok edilmiş yani tanımlı olmayan bir A’ya ulaşabilir ve bu da beklenmedik davranışlara sebep olabilir.
  2. std::shared_ptr: Bu durumda A ile B arasında shared_ptr’lar üzerinde bir döngü oluşur ve bu da hem A hem de B’nin yok edilmesini önler.
  3. std::weak_ptr: İşte bu kullanım yukarıdaki iki problemin de üstesinden gelir. Eğer A yok edilir ise B expired() API si aracılığı ile bunu öğrenebilir, ayrıca B’nin weak_ptr üzerinden A’yı adreslemesi A’nın referans adetini değiştirmediğinden A yokedilebilir ve 2. maddedeki durumunda önüne geçilmiş olur.

Evet arkadaşlar bu yazım ile birlikte akıllı işaretçiler konumuzu noktalıyoruz, tabi her şeyi anlattık mı muhakkak daha bir çok öğrenilecek husus var fakat bu iki yazı ile akıllı işaretçileri hemen kullanmaya başlayabilirsiniz. Son olarak akıllı işaretçiler metotlara geçirilmesi ve diğer bir takım hususlar için aşağıdaki yazıları incelemenizi öneririm. Görüşmek dileğiyle.

GotW #91 Solution: Smart Pointer Parameters

GotW #89 Solution: Smart Pointers

2 Comments Modern C++ (4) : Smart Pointers – II

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.