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
İçerik
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
class Thing { public: void doSomething(); }; ostream& operator<< (ostream&, const Thing&); ... // Bir fonksiyon shared_ptr dönebilir std::shared_ptr<Thing> returnSharedPtrInstance(); // Bir fonksiyon shared_ptr "pass by value" ile alabilir std::shared_ptr<Thing> useSharedPtr(std::shared_ptr<Thing> p); void foo() { // yeni bir shaerd_ptr oluşturmak için std::shared_ptr<Thing> p1(new Thing); // Yönetilen nesneye standart işaretçi dönmek için Thing* p1RawPtr = p1.get(); ... // p1 ve p2 Thing'in ortak sahipleri std::shared_ptr<Thing> p2 = p1; ... // farklı bir Thing nesnesi shared_ptr<Thing> p3(new Thing); // p1 artık ilk Thing nesnesini işaret etmiyor olabilir p1 = returnSharedPtrInstance(); useSharedPtr(p2); // Sınıfın metodunu standart işaretçi gibi çağırma p3->doSomething(); // Standart işaretçilerdeki "Dereference" işlemi akıllı işaretçiler için de geçerli std::cout << *p2 << std::endl; // İlgili shared_ptr nesnesinin işaret ettiği nesneye artık adresleme // Referans adetini bir düşür eğer sıfır ise nesneyi yoket p1.reset(); p2 = nullptr; } // p1, p2, p3 kapsam dışına çıktığı için referans adetlerini azalt ve Thing nesnelerini yok et |
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:
1 2 3 4 5 6 7 |
class Base {}; class Derived : public Base {}; ... Derived * dp1 = new Derived; Base * bp1 = dp1; Base * bp2(dp1); Base * bp3 = new Derived; |
Eğer shared_ptr tarafından yönetilen nesneler gerekli kopyalama kabiliyetlerini sunuyorlar ise aşağıdaki shared_ptr kullanımı gerçekleştirilebilmektedir:
1 2 3 4 5 6 7 |
class Base {}; class Derived : public Base {}; ... shared_ptr<Derived> dp1(new Derived); shared_ptr<Base> bp1 = dp1; shared_ptr<Base> bp2(dp1); shared_ptr<Base> bp3(new Derived); |
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.
1 2 3 4 5 |
shared_ptr<Base> base_ptr (new Base); shared_ptr<Derived> derived_ptr; // Eğer static_cast<Derived *>(base_ptr.get()) geçerli ise aşağıdaki // kullanım da geçerlidir derived_ptr = static_pointer_cast<Derived>(base_ptr); |
Ö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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// İşaret edilen nesneye ilişkin özelleşmiş silici auto monitorDeletion = [](Ornek *instance) { logDeletion(instance); delete instance; }; // Akıllı işaretçide özelleşmiş silicinin tipi tutuluyor std::unique_ptr<Ornek, decltype(monitorDeletion)> uniqInstance(new Ornek, monitorDeletion); // Akıllı işaretçinin kendisi tutuluyor std::shared_ptr<Ornek> sharedInstance(new instance, monitorDeletion); |
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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// pw standart işaretçi auto pw = new MyClass; // pw'nin yönetimi için sharedInstance1 bir kontrol bloğu oluşturuyor std::shared_ptr<MyClass> sharedInstance1(pw); // pw'nin yönetimi için sharedInstance2 yeni kontrol bloğu oluşturuyor std::shared_ptr<MyClass> sharedInstance2(pw); // Direk oluşturulan nesnenin geçirilmesi örneği std::shared_ptr<MyClass> directUsageSP(new MyClass); // Ortak kontrol bloğu kullanım örneği. Genel olarak beklenen kullanım şekli. // Burada tek bir kontrol bloğu var ve sadece referans adeti arttırılıyor. std::shared_ptr<MyClass> sharedInstance2(sharedInstance1); |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class SceneNode { public: ... void TraverseNodes(); ... protected: std::vector<std::shared_ptr<SceneNode>> visitedNodes; }; void SceneNode::TraverseNodes() { ... // Mevcut nesneye işaret eden standart işaretçi ile yeni bir shared_ptr // oluştur ve listeye ekle visitedNodes.emplace_back(this); } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class SceneNode : public std::enabled_shared_from_this<SceneNode> { public: ... void TraverseNodes(); ... protected: std::vector<std::shared_ptr<SceneNode>> visitedNodes; }; void SceneNode::TraverseNodes() { ... visitedNodes.emplace_back(shared_from_this()); } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// spw oluşturulduktan sonra buna olan referans adeti 1 olur. std::shared_ptr<Widget> spw = std::make_shared<Widget>(); … // wpw de aynı şekilde aynı Wıdget nesnesini işaret etmekte ve bu atama ile // referans adeti değişmez yani halen 1 dir. std::weak_ptr<Widget> wpw(spw); // Henüz referans adeti 1 olduğu için false döner if(wpw.expired() … // Artık Widget nesnesi yok edilir ve referans adeti 0 olur spw = nullptr; // Referans adeti 0 olduğu için true döner |
Ö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.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Eğer wpw'nin işare ettiği nesne yok edilmiş ise nullptr döner std::shared_ptr<Widget> spw1 = wpw.lock(); if (spw1) { // spw1'e eriş spw1->... } else { std::cout << "Nesne kullanilamaz!" << std::endl; } |
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?
Üç seçeneğimiz var:
- 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.
- 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.
- 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.
Çok teşekkürler emekleriniz için.
Rica ederim, faydalı olduysa ne mutlu bana.
Emeğiniz için çok teşekkürler, bi önceki yazınıza erişmeye çalıştığımda sayfa bulunamadı hatası alıyorum.
Sağlıcakla kalın.
Geri bildiriminiz için teşekkür ederim. İlgili bağlantıyı düzelttim. Keyifli okumalar.