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

Arayı çok açmadan bir sonraki Modern C++ yazımı da C++ 11 ile gelen bence en önemli özelliklerden biri olan ve her bir C++ geliştiricisinin günlük olarak kullanması gerektiğini düşündüğüm Akıllı İşaretçiler yani “Smart Pointer” konusuna ayıracağım.

Normalde bu konuyu tek bir yazıda ele alacaktım ama çok uzun olmasın diye hem de arayı da çok açmamak adına iki parça haline yayınlayacağım. İlk yazımda akıllı işaretçilere genel giriş/motivasyon ve unique_ptr konusuna dalcaz. Sonraki yazımda da shared_ptr ve weak_ptr konularına değineceğim.

Motivasyon:

Daha önceki yazılarımda da ifade ettiğim gibi Modern C++’ın getirdiği bence en önemli hususlardan birisi de dilde düşülen hataları minimize etmek. Tahmin edeceğiniz üzere de C ve C++ dillerinde yazılımcıların düştüğü hataların bir çoğunun kaynağının HATALI İşaretçiler kullanımı olduğunu görebilirsiniz. Hatalı diyorum çünkü işaretçiler aslında doğru kullanıldığında gerçekten oldukça güçlü bir araç. Bu noktada sahiplik ya da “Ownership” mevzusuna girmekte fayda var. Özellikle dinamik olarak oluşturulan (yani “new” operatörü kullanılarak oluşturulan, artık malloc tan bahsetmek bile istemiyorum 🙂 nesneler için sahiplik çok önemlidir.

Bir nesnenin sahibi olmak demek onun hayat döngüsünü de yönetmek anlamına geliyor. Bu da onu oluşturduğumuz gibi “delete” operatörü ile ne zaman sileceğimiz sorumluluğunu da yüklenmemiz gerektiği anlamına geliyor (kendinizi bir an çocuk yetişdirme yazısında bulduğunuz düşündürttüysem üzgünüm :). Bu noktada sahiplik mekanizmasının doğru bir şekilde işletilmemesi durumunda işaretçiler ile
ilgili problemler ortaya çıkmaya başlıyor:

  • Bellek sızıntıları,
  • Artık var olmayan/silinmiş işaretçilere erişim veya tekrar silmeye çalışmak,
  • Herhangi bir yere işaret etmeyen işaretçilerin kullanılması.

Bunları da bellek sızıntısı ve bellek bozulması altında toplayabiliriz. Bunlar ve daha detaylı işaretçi hususları için C++ duayenlerinden olan Scott Meyers’e kulak verelim. Kendisi kitabında standart işaretçilere ilişkin aşağıdaki sıkıntılardan bahseder:

1) “İşaretçilerin deklarasyonundan ilgili işaretçinin tek bir nesneye mi yoksa bir diziye mi işaret ettiği anlaşılamamakta.”
Burada biz de bir basit bir örnek verelim:

2) Deklarasyona bakarak ilgili nesne ile işiniz bitti mi onu silip/silemeyeceğimize ilişkin herhangi bir bilgi sunmaz. Başka birisi de aynı zamanda kullanıyor olabilir.

3) Velev ki onu silmeniz gerektiğini biliyorsunuz, yani kontrol size. Fakat bu sefer de nasıl sileceğinizi bilemeyebilirsiniz. Yani delete operatörünü mü kullanmalısınız, yoksa silme işlemi için ayrı bir metoda mı geçirmelisiniz. Bunu bilmenize imkan yok.

4) Yine diyelim ki silme için özel bir metot olmadığını biliyorsunuz. Yani delete operatörü kullanılacak. Bu durumda da ilk madde ile ilintili olarak delete mi delete [] operatörü mü kullanılacak bilemeyebilirsiniz.

5) Yukarıdaki her hususu biliyor olsanız bile, kodun herhangi bir yerinde belirttiğiniz kaynağın bir kere silindiğinden ya da herhangi bir kullanıcı tarafından silindiğinden emin olamazsınız. Bu da aslında yazımın başında bahsettiğim sıkıntıları doğurabilir.

6) Bir diğer sıkıntı da herhangi bir işaretçinin o an geçerli bir veriyi adresleyip/adreslemediğini bilmenin bir yolunun olmamasıdır.

İşte akıllı işaretçiler de bu hususların üstesinden gelmek için geliştirilerek dile dahil edilen yapılardır. C++ kütüphanelerinin en ünlülerinden olan Boost kullanıyor iseniz, boost akıllı işaretçilere uzunca bir süredir sahip. C++ 11 ile gelen kabiliyetlerin bir kısmının da buradan geldiğini söylemek haksızlık olmaz diye düşünüyorum.

Akıllı İşaretçiler

Akıllı işaretçiler temel olarak, işaretçiler üzerine çıkılan ve ek olarak bu işaretçilerin yaşam döngüleri kontrol etmek adına basitçe “reference counting” benzeri yapıları uygulayan sınıflardır. Yani ilgili nesneleri oluşturup daha sonra bunların gerekli olduğu zaman silinmesi kontrolünü üzerine alan “template” sınıflardır.
Temel olarak normal işaretçiler ile yapabildiğiniz her şeyi akıllı işaretçiler ile de yapabilirsiniz, üstüne daha kontrollü ve güvenli bir işaretçi kullanımına sahip oluyorsunuz.
Kodta normal işaretçileri kullandığınız her yerde akıllı işaretçileri kullanabilirsiniz.

C++ 11 ile üç temel akıllı işaretçi gelmektedir. Bunlar std::shared_ptr, std::unique_ptr ve std::weak_ptr’dır. std::auto_ptr da var lakin kendisi C++11 ile “deprecated” olarak işaretlenmiş,
yani kullanılmaması önerilmiş ve C++ 17 ile de tamamen kaldırılmıştır. std::auto_ptr ile yapılabilecek her şet std::auto_ptr ile yapılabilmektedir.

Bunları kodunuzda kullanmak için #include <memory> başlığını eklemeniz gerekmektedir.
Bu sınıfları oluşturmak için aşağıdaki yöntemi izleyebilirsiniz.

E ama ben aşağıdaki gibi de oluşturabilirim diye düşünüyordum.

Evet oluşturabilirsin ama bu yapılar C++ 14 ile geldi sevgili dostum. Bu arada eğer derleyiciniz destekliyor ise ikinci yöntem her zaman daha temiz ve tavsiye edilmektedir.
Ha kardeşim benim kodum C++ 11 için de çalışsın hemi de make_shared yapacağım diyorsan aşağıdaki kodu al kullan. Buradaki “…” da C++ 11 ile gelen başka bir mevzu (variadic templates).

Bu sınıfların detaylarına geçmeden önce birer cümle ile bu sınıfları özetlemeye çalışalım.
– Başka sınıflar ile paylaşılması planlanmayan aynı anda tek bir sahibi olacak sınıflar için kullanılabilir (exclusive/siıngular-ownership resource management)
– Başka sınıflar ile paylaşılması planlanan aynı anda birden fazla sahibi olarak sınıflar için kullanılabilir (shared-ownership resource management)
std::shared_ptr ile oluşturulmuş olan sınıfların durumunu sorgulamak (bir nebze gözlemci olarak düşünülebilir) için kullanılabilecek ve pek te akıllı olmayan sınıf 🙂

std::unique_ptr

Yukarıda da bahsettiğimiz üzere unique_ptr’ı başka sınıflar ile paylaşmayı düşünmediğiniz ve dinamik olarak oluşturacağınız sınıflar için kullanabilirsiniz. Tanımlandığı
kapsam dışına çıktığı anda işaret ettiği nesne otomatik olarak silinecektir. Kısacası normal işaretçileri kullandığınız çoğu yerde unique_ptr’ı gönül rahatlığı ile kullanabilirsiniz.

Peki neden unique_ptr?

Öncelikle unique_ptr altında yatan mekanizma oldukça basit ve overhead/ekstra iş yükü çok az ve performans anlamında normal işaretçilere ile aynı seviyede.
Peki neden? Çünkü shared_ptr da olduğu gibi paylaşılan nesnelerin yönetilmesi için gerekli olan dinamik yer almalara veya yönetim işlerine ihtiyaç yok. Ya bir nesneyi işaret
ediyor ya da etmiyor bu kadar. Boyut anlamında da normal işaretçilere ek bir şey tutulmuyor 🙂 Kısacası performansmış yok fazla yer kaplarmış gibi kaygılar yersiz.

unique_ptr‘ları kopyalayamazsınız! Tek sahip diyorum, paylaşmak yok diyorum. Fakat sahipliği transfer edebilirsiniz. Hobaaa! Transfer etmek ne demek şimdi 🙂 Bu konuya detaylı olarak
başka bir yazımda gireceğim şimdilik aşağıdaki örnek ile olayı çok çok özet bir şekilde anlatayım.

Normal şartlar altında unique_ptr sahip olduğu dinamik nesneyi ilgili kapsam dışına çıkınca yok eder. Bunu el ile yapmak için:

Ayrıca sahipliği devretmek için relese() apisi kullanılabilir. Bu metot çağrıldıktan sonra standart işaretçi olarak dönülen nesnenin yaşam döngüsünden artık unique_ptr sorumlu değildir ve unique_ptr bu çağrıdan sonra boş hale gelir.

Velev ki unique_ptr olarak bir nesne oluşturdunuz ve bunu paylaşmak istiyorsunuz ne yapacaksınız.

unique_ptr‘ı STL konteynerlarda da kullanabilirsiniz. Kullanım esnasında aşağıdaki hususlara dikkat etmenizde fayda var:
– Konteynerları rvalue olarak ya da std::move ile sahipliğini tranfer ederek doldurun,
– Konteynerdan bir eleman sildiğinizde onun sahip olduğu nesnenin de silineceğini unutmayın.
Aynı şekilde clear API sinin çağrılması durumunda bütün elemanlar silinecektir.

Son olarak unique_ptr‘ı tek bir nesne için oluşturabildiğiniz gibi dizi olarak ta oluşturabilirsiniz. Muhtemelen std::array, vector ve benzeri yapılar fazlasıyla işinizi görecek olsa da
bu tarz bir ihtiyaç hasıl olursa aşağıdaki gibi bir kullanımı mevcut ve tabiki delete [] sizin el ile çağırmanıza gerek yok 😉

Özet olarak;
std::unique_ptr küçük, hızlı ve sadece transfer edilebilen tek sahiplik yaklaşımına sahip akıllı işaretçisidir.
Varsayılan olarak kaynaklar delete operatörü ile silinmektedir, fakat özelleşmiş siliciler tanımlanabilir tabi bunlar unique_ptr nesnelerinin boyutlarını arttıracağını unutmayın.
unique_ptr ile oluşturulan kaynaklarınızı shared_ptr haline dönüştürebilirsiniz.

Yine bir diğer C++ duayeni olan ve C++ alanında oldukça aktif olarak çalışan Herb Sutter’in sitesindeki akıllı işaretçiler yazılarına göz atabilirsiniz (son iki referans). Oranın da bir özetini ikinci yazıma ekleyeceğim inşallah.

Bir sonraki yazımda görüşmek üzere kendinize iyi bakın sevgili yazılımperver dostlarım…

Referanslar

– http://www.boost.org/doc/libs/1_62_0/libs/smart_ptr/shared_ptr.htm
– https://www.safaribooksonline.com/library/view/effective-modern-c/9781491908419/ch04.html
– http://www.drdobbs.com/cpp/c11-uniqueptr/240002708
– http://en.cppreference.com/w/cpp/memory/unique_ptr
– https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/
– https://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers/

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.