Pointer (Raw Pointer) 'lar hafızada belirli bir alanın (genelde dinamik bir alanın) adresini gösterir ve ilgili alan ile programcının işi bittiğinde bu alanın sisteme iade edilmesi programcıya bırakılmıştır. C dilinde bunun doğal şekli budur ve kontrol tamamen programcıya aittir. C++ 'da bulunan smart pointer 'lar sayesinde ise dinamik alanın iade edilmesi otomatik olarak uygun bir zamanda gerçekleştirilir. Bu da programcının gözünden kaçabilecek hataların önlenmesi anlamına gelmektedir. Yazımızın konusu olan unique_ptr ise; C++11 standartları ile birlikte gelen akıllı gösterici (smart pointer) türü sınıfındandır. unique_ptr sınıfı C++98 standartlarında var olan tasarımsal olarak çok kullanışlı olmayan auto_ptr sınıfının yerine getirilmiştir. auto_ptr 'de bulunan ve hataya sebep olabilecek birden fazla sahiplik özelliği yoktur, tek bir sahiplik (exclusive ownership) vardır. Bir unique_ptr nesnesi, kendi hayatı sona erince işaret ettiği dinamik alanı da iade ettiğinden kaynakların sisteme iade edilmesini sağlar. Copy constructor, operator= fonksiyonları delete edildiğinden dolayısıyla kullanıcı herhangi bir yerde herhangi bir şekilde nesneyi kopyalayamayacaktır. Bu noktada sorumluluğu programcıya bırakılan 2 noktayı yazının devamında açıklayacağız. Son olarak C++11 standartları ile auto_ptr sınıfı kullanımdan düşürülmüş (deprecated)' tür.
Bir programcının bakış açısında fonksiyonlar işlerini yaparken gerçekleştirdikleri adımlar kabaca şu şekildedir:
- Önce işlerini gerçekleştirebilmek için bazı kaynaklar edinirler veya var olan kaynaklarının adresleri aktarılır
- Sorumlusu oldukları işleri gerçekleştirirler
- İşlerini tamamladıktan sonra kendi edindikleri/sorumlusu oldukları kaynakları sisteme geri verirler. Bu noktada şu durumu da göz önünde bulunduralım. İşlerini tamamlayamadan bir hata durumunda bu kaynakların akıbeti ne olacak? Raw pointerlarla çalışıldığında tüm sorumluluk programcıdadır.
Şimdi unique_ptr bize yukarıda kaynaklar/hata durumundaki senaryolara karşı sağladıklarını ve genel özelliklerini inceleyelim.
Aşağıdaki örnekte bize bir unique_ptr nesnesinin değer yoluyla aktarılamıyacağının altı çizilmiş. Çünkü unique_ptr nesnesinin copy constructor'ı bulunmamaktadır. Peki değer yoluyla katarım yapılsaydı ne mi olurdu? func fonksiyonun ptr parametresi uptr'nin işaret ettiği Bird nesnesini işaret edecekti ve fonksiyonun işi bittiğinde ptr değişkeninin ömrü de biteceğinden işaret ettiği kaynağı sisteme iade edecek ve bunun sonucu olarak da programın devamında hatayla karşılaşılacaktı.
Bu durumda karşımıza 2 seçenek çıkmaktadır ya referans yoluyla aktarım ya da ileride görebileceğimiz std::move ile sahipliğini başka bir unique_ptr 'ye geçirmek. Biz aşağıdaki örnekte referans yoluyla aktarım yaparak programın çalışabilir hale gelmesini sağladık.
Bir unique_ptr nesnesinin işaret ettiği bir nesne var mı yok mu sorusuna cevap olabilecek 3 farklı yöntem mevcuttur. Bunlar:
- <unique_ptr değişkeni>
- <unique_ptr değişkeni> == nullptr
- <unique_ptr değişkeni>.get() == nullptr
Yukarıda görülebileceği üzere uptr nesnesi hiç bir yeri işaret etmediği için üç durumda da if deyimi içerisine girmiştir. Aşağıdaki örnekte ise aynı testi bir değeri işaret ettiğinde uygularsak;
<unique_ptr değişkeni>.release() fonksiyonu ise sorumluluğunu aldığı dinamik nesneyi bırakır ve geri dönüş değeri olarak bu nesnenin adresini geri döner. Artık bu noktadan sonra bu yerle ilgili sorumluluk smart pointer'dan çıkmış programcıya geçmiştir. Bu noktadan sonra unique_ptr değişkeni hiçbir yeri işaret etmemektedir. Aşağıda bu durumla ilgili örnek incelenebilir.
unique_ptr sınıfı tek sahiplik semantiğini uygular. Başta söylediğimiz
sorumluluğu programcıya bırakılan 2 noktadan birincisi: birden fazla unique_ptr nesnesinin aynı dinamik nesnenin adresiyle başlatılıp/başlatılmaması programcının sorumluluğundadır.
Yukarıdaki örnekte iki unique_ptr nesnesi aynı yeri göstermiş ve program sonlanırken birinci unique_ptr kaynakları iade etmiş ve ikincide aynı adresi iade etmeye çalıştığı için program çalışma anında hatayla karşılaşmıştır. Böyle bir durumda sorumluluk yukarıda da dediğimiz gibi tamamen programcının sorumluluğundadır.
Bir unique_ptr nesnesini kopyalama semantiğiyle hayata başlatamamayız ve bir unique_ptr nesnesine kopyalama semantiğiyle atama yapamayız. unique_ptr sınıfında taşıma semantiği kullanılarak atama veya ilk değer verme işlemlerini gerçekleştirebiliriz. Aşağıdaki örnekte kopyalama ve taşıma ile ilk değer verilmeye çalışılmış ve kopyalama ile yapılamayacağına ilişkin noktanın altı çizilmiştir.
Bir unique_ptr nesnesine ait sahipliğin std::move sahipliğinin devredildiğini aşağıdaki örnekte görebilirsiniz. uptr1 artık hiçbir yeri göstermemektedir.
Aşağıdaki örnekte ise bir unique_ptr nesnesine atama ile ilk değer verilmesini görebiliriz.
Bir unique_ptr nesnesine std::move ile atama yapılması durumunda daha önce gösterdiği bir yer varsa ona ait kaynaklar iade edilmekte daha sonra yeni alanın sorumluluğu alınmaktadır. Aşağıdaki örnekte uptr2'nin gösterdiği alan iade edilmiş ve destructor'ın çağrıldığı ve ardından
uptr1'in gösterdiği alanın sorumluluğu uptr2'ye geçtiğini görebiliriz.
Bir unique_ptr nesnesine ait reset() fonksiyonunu çağırmak işaret ettiği kaynakların iade edilmesi anlamına gelmektedir. Aşağıda buna ilişkin örneği görebilirsiniz.
Bir unique_ptr nesnesine nullptr değerinin atanması nesnenin reset() fonksiyonunun çağrılmasına eşdeğerdir. Aşağıda buna ilişkin örneği görebilirsiniz.
Bir unique_ptr nesnesine ait release fonksiyonunu çağırmak işaret ettiği kaynakların iade edileceği anlamına gelmemektedir. Sadece sorumluluğun bırakılması anlamına gelmektedir. Aşağıdaki örnekte destructor'ın çağrılmadığını görebiliriz.