4 Ekim 2017 Çarşamba

C++ Smart Pointers - Akıllı İşaretçiler

C++'da new ile atanan alanların yönetimini manuel olarak yapmamız gerekiyor.
Garbage collector olmadığı için üretilen nesnelerin delete ile silinmesi gerekiyor.
Bu durum gözden kaçırmalarla dangling pointer, memory leak vb. bellek problemlerine sebep olabilir.

Bellek atamaları konusunda daha temkinli olmak için C++ tarafından önerilen yöntem ise smart pointers (akıllı işaretçiler). Bu işaretçiler aslında bir sınıfın nesneleri olarak üretiliyor ve etki alanlarının dışına çıktıklarında sınıflarında bulunan yıkıcılar (destructor) tarafından otomatik olarak siliniyor. Bu sebeple ürettiğimiz objeleri endişe duymadan kullanabiliriz :). Smart pointer'ların bir diğer amacı ise ham (raw, built-in) işaretçilerle aynı söz dizimine sahip olarak kullanılmaktır (-> ve * operatörleri).

Smart pointer'ları std kütüphanesinden alacağımız için #include <memory> 'yi eklemek yeterli.
Bu yazıda auto_ptr artık kullanılmadığı için shared_ptr, weak_ptr ve unique_ptr 'den bahsedeceğim.

== shared_ptr == 
En kapsamlı akıllı işaretçidir. Kullanımı ham işaretçilere en çok benzeyendir. Bir shared_ptr oluşturulduğunda içerisinde referenced-counter adında iki farklı sayaç tutan bir yapı oluşur. Bu sayaçlardan birisi weak_ptr'leri, diğeri ise shared_ptr'leri tutar.

class Foo { ...... };
shared_ptr<Foo> sp1(new Foo);

Buradaki gibi üretildiğinde; shared_ptr, manager obj. ve managed obj. olmak üzere üç tane nesne oluşur.
Bu shared_ptr'ye yeni spr'ler (shared pointer) eklendikçe share sayacı, weak_ptr'ler eklendikçede weak sayacı artar. Aynı şekilde silindikçe de azalır. Ortamda en az bir spr varsa, managed object hayatta kalır. manager objenin ayakta kalması için ise en az bir wpr (weak pointer) ya da spr'nin olması yeterlidir. Eğer ortamda hiç spr yok ama wpr var ise managed obj. silinir, bu objeye spr eklendiğinde yeninden managed obj. oluşur.
shared_ptr'ler, ham işaretçiler gibi dereference edilebilir (*) ve -> ile fonksiyonlarına erişilebilir.

Bir shared_ptr'ye başka shared pointer'lar ve weak pointer'lar eklendikçe durumu aşağıdaki gibi olacaktır:

class Thing {
public:
void defrangulate();
};
shared_ptr<Thing> find_something();
shared_ptr<Thing> do_something_with(shared_ptr <Thing> p);
void foo() {
shared_ptr<Thing> spr1(new Thing);
shared_ptr<Thing> spr2 = spr1;
shared_ptr<Thing> spr3(new Thing);
spr1 = find_something();
do_something_with(spr2);
spr3->defrangulate();
cout << *spr2 << endl;
spr1.reset(); // decrement count, delete if last
spr2 = nullptr; // convert nullptr,  to an empty shared_ptr and decrement count
}
Yukarıdaki örnekteki gibi shared_ptr'yi başka bir spr'ye kopyalarak sayacı bir artırabiliriz. Smart pointer'ları delete ile silmeye çalışmak derleme hatasına neden olacaktır ancak .reset() ile sayacı bir azaltabilir ya da başka bir spr olarak atayabiliriz. .use_count() ise aynı managed obj.'ye kaç tane shared_ptr'nin işaret ettiğini gösterir.

Bir smart pointer'ı, direkt olarak raw pointer'a atayamayız.
shared_ptr<Thing> sp;
Thing *raw_ptr = new Thing;
sp = raw_ptr; // compile error!!!!!!!!
Shared pointer'dan, raw ptr'yi elde etmenin tek yolu .get() kullanmaktır.
Kalıtımda da herhangi bir problem olmadan aşağıdaki gibi kullanabiliriz:

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);

pointer'lar arasında dönüştürme işlemini static_pointer_cast kullanarak yapabiliriz.
shared_ptr <Base> baseptr(new Base);
shared_ptr<Derived> derived_ptr;
derived_ptr = static_pointer_cast<Derived>(base_ptr);
Artık derived_ptr, base_ptr oldu.

İki shared_ptr'yi ==, != operatörleri ile karşılaştırabiliriz. Aynı şekilde if (spr) {..} true dönerse, spr bir objeye işaret ediyor demektir.

Örn. spr3.reset(); dedikten sonra .use_count() sıfır döndürmeye başladıysa, if (spr3) {...} false dönecektir.

Not1: Aynı nesneye işaret eden raw ve smart pointer'ı aynı anda kullanmak tavsiye edilmez, aynı nesneyi iki kez silmeye ya da dangling pointer'a sebep olabilir.

Not2: shared pointer üretirken new ile üretmek iki kere alan tahsisi yapmaya neden olacak ve daha maliyetli olacaktır (İlki new için, ikincisi shared pointer için). Bunun yerine make_shared ifadesini kullanarak bu büyük alan tahsisini tek seferde yapmaya indiregeyebiliriz.

shared_ptr <Thing> spr(new Thing); // two allocations
shared_ptr <Thing> sp(make_shared<Thing>()); // only one allocation

== weak_ptr ==
weak_ptr'ler ancak başka bir weak_ptr ya da shared_ptr sayesinde var olabilirler. Ait oldukları nesnenin yaşam süresine etki etmezler. Dereference (*) etme ve -> operatörü weak_ptr için tanımlı değildir. Bu sebeple weak_ptr'ler zayıf referanslardır (weak reference), genelde ait oldukları shared_ptr'nin hala geçerli olup olmadığını kontrol etmek ya da shared_ptr'ler arasında döngü oluşturan referansları (cycle reference) kaldırmak için kullanılırlar.
.reset() ile içi boşaltılabilir ancak nullptr ataması yapmak mümkün değildir.
weak_ptr'den shared_ptr elde etmek için .lock() kullanılır.
shared_ptr<Thing> sp2 = wp2.lock();
shared_ptr<Thing> sp(new Thing);
weak_ptr<Thing> wp1(sp); // construct wp1 from a shared_ptr
weak_ptr<Thing> wp2; // an empty weak_ptr - points to nothing
wp2 = sp; // wp2 now points to the new Thing
weak_ptr<Thing> wp3(wp2); // construct wp3 from a weak_ptr
weak_ptr<Thing> wp4 = wp2; // wp4 now points to the new Thing

shared_ptr konusunda gördüğümüz shared pointer'ları birbirine atama işlemi döngü referanslarına sebep olmaz. Bu sebeple weak_ptr kullanmak gerekmez.
shared_ptr<Thing> p1(new Thing);
shared_ptr<Thing> p2 = p1;
shared_ptr<Thing> p3 = p2;
shared_ptr<Thing> p4 = p3;

Yukarıda oluşan sadece aynı nesneye referans eden birden çok pointer olmasıdır. Döngü referansları için buradaki örneği inceleyebilirsiniz. Burada örnek çok açık ve temel görünüyor. Muhtemelen çok daha büyük kod bloklarında daha karmaşık bir şekilde döngülerin oluşmasına sebep olunuyordur diye düşünüyorum :). Bu sebeple smart pointer'ları kullanırken dikkatli olmak gerek.

weak_ptr'ler ile dangling pointer'lara engel olma:
shared_ptr<int> sptr;
*sptr = 10;
weak_ptr<int> wptr;
wptr = sptr;
sptr.reset();
.....
..
if (wptr.lock()) { ... }
 // burada direkt olarak sptr üzerinden
 //kullansaydık içeriği boş olan bir pointer kullanmış olacaktık

weak pointer'larda önemli durumlardan bir diğeri ise this objesinin kullanımı. Tamamen ham işaretçilerle kullanımında bir problem çıkmazken, akıllı işaretçilerde problem oluşabilir.
class Thing
{
    public:
        Thing() {cout << "Thing....\n";}
        ~Thing() {cout << "~Thing....\n";}
        void foo();
};

void set_x(shared_ptr<Thing> ptr) {
   ....
}

void Thing::foo()
{
    cout << "Thing::foo() ... \n";
    shared_ptr<Thing> sp(this);
    set_x(sp);
}

int main()
{
  shared_ptr<Thing> t2(new Thing);
  t2->foo();
 ...
}
Burada this raw objesinden yeni bir shared_ptr daha üretiliyor ve bu raw obje aslında daha önceden başka bir shared pointer için kullanılmış. Bu durumda this'in shared_ptr'si silindiğinde, t2 de silinmiş olacak ve t2 silinmeye çalışıldığında zaten silinmiş bir obje tekrar silinmeye çalışılıyor olacak..
Bunu önlemek için kullanılan ifade ise: enable_shared_from_this
class Thing: public enable_shared_from_this()
{
    public:
        Thing() {cout << "Thing....\n";}
        ~Thing() {cout << "~Thing....\n";}
        void foo();
};
...
void Thing::foo()
{
    cout << "Thing::foo() ... \n";
    shared_ptr<Thing> sp = shared_from_this();
    set_x(sp);
}
...

== unique_ptr ==
unique_ptr'de etki alanının dışına çıkıldığında işaret edilen obje silinir. shared_ptr'ye biraz benziyor ancak referenced-counter yok, bu sebeple daha az maliyetli. Sahiplik hakkı ise objeye özel, paylaşımlı bir yapı yok. Copy construction ve copy assignment olmadığı için bir unique_ptr'yi başka bir unique_ptr'ye atayamayız ya da kopyalamayız ancak sahipliği fonksiyondan geri döndürerek bir başka işaretçiye taşıyabiliriz.
unique_ptr<Thing> p1(new Thing);
unique_ptr<Thing> p2(p1); // error, copy construction not allowed
unique_ptr<Thing> p3;
p3 = p1; // error, copy assignment not allowed
..

unique_ptr<Thing> create_unique_ptr()
{
  unique_ptr<Thing> local_ptr(new Thing);
  return local_ptr;
}

unique_ptr<Thing> uptr;
uptr = create_unique_ptr();

Ya da move() fonksiyonu kullanarak da sahiplik hakkı taşıyabiliriz.

Kaynaklar:
http://umich.edu/~eecs381/handouts/C++11_smart_ptrs.pdf
https://cppturkiye.wordpress.com/2016/01/25/c11-zeki-akilli-gostericiler-smart-pointers/