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

Güncelleme (auto_ptr vs unique_ptr):

Tekrar merhaba arkadaşlar, gelen bir iki yorum sonrasında bir konuyu açıklığa kavuşturmakta fayda olduğunu düşündüm. Bu da std::unique_ptr ile std::auto_ptr arasındaki fark. Gerçi yazımda, std::auto_ptr’ın pek bir anlamı kalmadığını, bu amaçla std::unique_ptr kullanılabileceğini ifade etmiştim ama meraklı arkadaşlar için bir kaç kelam etmekte bir sakınca yok.

auto_ptr’ın kopya yapıcısı ve atama operatörü, tutulan işaretçiyi kopyalamamakta, sadece transfer etmektedir ve kopyalanan ilk auto_ptr nesnesini boş bırakmaktadır. Hemen bir örnek kod ile inceleyelim:

Yukarıdaki kodu çalıştırdığınızda, ilgili kabiliyetin “depreceated” olduğuna ilişkin uyarı yanında aşağıdakine benzer bir çıktı alacaksınız:

Bu kabiliyet C++ 11 ile onaylanmamış olarak etiketlenmiş ve C++ 17 ile de tamamen kaldırılmıştır. Bu sınıfın  sıkıntısı, atama operatörü ile birlikte önceki nesneyi sıfırlamasından ötürü, STL konteynerlerinde kullanılamaması ve C++ 11 ile gelen std::unique_ptr’ın, taşıma mekanikleri ile daha emniyetli bir mekanizma sunmasıdır. Uzun lafın kısası, std::auto_ptr, yerine std::unique_ptr kullanabilirsiniz.

Şimdi orjinal yazıya dönebiliriz 🙂

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 dalacağız. Sonraki yazımda da, shared_ptr ve weak_ptr konularına değineceğim.

İkinci yazım için:

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

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 şey std::unique_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-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
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/

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

  1. Ramazan

    Teşekkürler yazı için gayet anlaşılır olmuş. Hatta anlatım şekline bayıldım. Yeni başlayanlar için daha anlaşılır. Ama bazı yerlerde yazım hataları ve benzer hatalar var.
    “std::auto_ptr ile yapılabilecek her şet std::auto_ptr ile yapılabilmektedir.” gibi.

    Reply
    1. yazılımperver

      Öncelikle yorumlar silinmiyor, çok fazla spam olduğu için onaylanmadan görünmüyor. Soruna gelince, yazıda her ne kadar bu özelliğin C++ 17 ile silindiğini belirtsemde, biraz daha açıklamak adına, yazının başına açıklama ve örnek kod ekledim, inceleyebilirsin.

      Reply

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.