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:
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 |
// C++ program to illustrate the use of auto_ptr #include <iostream> #include <memory> class A { public: void show() { std::cout << "A::show()\n"; } }; int main() { std::auto_ptr<A> p1(new A); p1->show(); std::cout << p1.get() << "\n"; // Kopya yapıcı çağrılır ve p1 boşaltılır. std::auto_ptr<A> p2(p1); p2->show(); // p1 artık boş std::cout << p1.get() << "\n"; // p1 artık p2'de std::cout << p2.get() << "\n"; return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
main.cpp:21:10: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations] 21 | std::auto_ptr<A> p1(new A); | ^~~~~~~~ In file included from /usr/include/c++/9/memory:80, from main.cpp:12: /usr/include/c++/9/bits/unique_ptr.h:53:28: note: declared here 53 | template<typename> class auto_ptr; | ^~~~~~~~ main.cpp:27:10: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations] 27 | std::auto_ptr<A> p2(p1); | ^~~~~~~~ In file included from /usr/include/c++/9/memory:80, from main.cpp:12: /usr/include/c++/9/bits/unique_ptr.h:53:28: note: declared here 53 | template<typename> class auto_ptr; | ^~~~~~~~ A::show() 0x55af16d93eb0 A::show() 0 0x55af16d93eb0 |
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:
1 2 3 4 5 6 7 8 |
int deneme = 3; int denemeArray[] = {1, 2, 3, 4, 5}; // Burada denemePtr basit veri yapısına işaret ediyor int* denemePtr = &deneme; // Burada denemePtr bir diziye işaret ediyor int* denemePtr = &denemeArray[0]; |
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.
1 2 |
shared_ptr<Elma> sharedPtrExample (new Elma); unique_ptr<Elma> uniquePtrExample (new Elma); |
E ama ben aşağıdaki gibi de oluşturabilirim diye düşünüyordum.
1 2 |
auto sharedPtrExample = make_shared<Elma>(); auto uniquePtrExample = make_unique<Elma>(); |
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).
1 2 3 4 5 6 7 8 9 10 11 |
template<typename T, typename... Ts> std::shared_ptr<T> make_shared(Ts&&... params) { return std::shared_ptr<T>(new T(std::forward<Ts>(params)...)); } template<typename T, typename... Ts> std::unique_ptr<T> make_unique(Ts&&... params) { return std::unique_ptr<T>(new T(std::forward<Ts>(params)...)); } |
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.
1 2 3 4 5 6 7 8 |
void Foo() { // instance oluşturulan DynamicObjectType nesnesinin tek sahibi std::unique_ptr<DynamicObjectType> instance(new DynamicObjectType); // Bir şeyler yap instance->DoSomething(); } // instance tanımlandığı fonksiyon kapsamından çıktığı için yok edilir o da otomatik olarak DynamicObjectType nesnesini siler |
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.
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 |
void TakeOwnership(std::unique_ptr<int> obj) { .... } // HATA: int'ten std::unique_ptr<int>'e dönüşüm yapamazsın TakeOwnership(3); int* p = new int(3); // HATA: int*'ten 'std::unique_ptr<int>'e dönüşüm yapamazsın. Explicit constructor TakeOwnership(p); // 3 un tek sahibi u auto u = std::make_unique<int>(3); // Kopyalama yasak TakeOwnership(u); <strong>// Ahanda işte transfer etme olayı bu</strong> TakeOwnership(std::move(u)); // Yeni bir sahiplik oluşturup verme bu da tamam TakeOwnership(std::make_unique<int>(3)); // Bu da OK TakeOwnership(nullptr); |
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:
1 2 3 4 |
std::unique_ptr<Elma> obj(new Elma); // Artık obj boş ve obj'nin sahip olduğu nesneye rawObj üzerinden erişilebilir. Sahiplik rawObj'de. Elma* rawObj = obj.release(); |
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.
1 2 3 4 |
std::unique_ptr<Elma> obj(new Elma); // Artık obj boş ve obj'nin sahip olduğu nesneye rawObj üzerinden erişilebilir. Sahiplik rawObj'de. Elma* rawObj = obj.release(); |
Velev ki unique_ptr olarak bir nesne oluşturdunuz ve bunu paylaşmak istiyorsunuz ne yapacaksınız.
1 2 3 4 5 6 7 |
std::unique_ptr<Karpuz> tekKisilikKarpuz(new Karpuz); // Sahipligi devrediyoruz std::shared_ptr<Karpuz> paylasilanKarpuz = std::move(tekKisilikKarpuz); // Ya da asagidaki ile de sahipligi devredebilirsiniz std::shared_ptr<Karpuz> paylasilanKarpuz(tekKisilikKarpuz.release()); |
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.
1 2 3 4 5 |
std::vector<unique_ptr<MyClass>> v; v.push_back(unique_ptr<MyClass>("hello world-I")); v.push_back(unique_ptr<MyClass>("hello world-II")); v.push_back(unique_ptr<MyClass>("hello world-III")); |
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 😉
1 2 3 4 5 6 7 |
std::unique_ptr<int[]> diziOrnek (new int[5]); for (int i=0; i < 5; ++i) diziOrnek[i] = i; for (int i=0; i<5; ++i) std::cout << foo[i] << ' '; |
Ö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/
Teşekkürler Türkçe olarak yazdığınız türden birşeyler bulmak oldukça zor.
Tek eksiğiniz sistemleştirmiyor oluşunuz.
Geri bildirim için çok teşekkürler. Aslında, düzenli olarak modern C++ üzerine yazı paylaşmaya gayret ediyorum, elbette zaman elverdiği müddetçe. C++ ve benzeri konulardaki yazı dizileri için,
Yazı Dizileri ve Örnek Kodlar sayfası yardımcı olur umarım.
shared_ptr’den bahsetmemişsin
Yazının başını okumamışsın sanırı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.”
shared_ptr için
Modern C++ (4) : Smart Pointers – II
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.
Teşekkürler.
unique_prt ile auto unique_ptr arasındaki fark nedir ?
unique_ptr ile auto unique_ptr arkasındaki fark nedir ?
Ö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.
Soru soruyoruz siliniyor böyle birşey yok.
Sevgili dostum siliniyor değil, henüz bakmaya fırsatım olmadığı için yorumu onaylamadım. En kısa sürede, cevap yazmaya çalışacağım. Biraz sabırlı olabilirsen, daha doyurucu cevap yazmaya çalışacağım ama belli ki çok acelen var, o zaman aşağıdaki bağlantıya bak.https://letmegooglethat.com/?q=unique_ptr+vs+auto_ptr+