C++11 – Zeki, Akıllı Göstericiler (Smart Pointers)

C++ programlama dili, atası C dilinden aldığı özelliklerden biri de memory yönetimidir. Ham göstericilerle (raw pointer) rahatlıkla hafızayı yönetebilirsiniz. ‘new’ operatörü ile aldığınız bellek bloğunu kolaylıkla ‘delete’ operatörüyle iade edebilriz. Fakat bu beraberinde riskleri de beraberinde getiriyor.

Memoryye iade edilmeyen yani ‘delete’ edilmeyen nesneler üzerinden bu bellek bloğuna erişilmeye çalışıldığında çalışma zamanı hatalarıyla (run time error) karşılaşıyor, signal 11 (segmentation fault) gibi kernelden uyarılar alabilirsiniz. İşte bu noktada C++’ ın en güçlü yanlarından olan RAII’ ye dayanan dinamik memory tahsisini de nesnelerin ömürlerine bağlayan sınıflar tasarlandı. Kullanım amaçlarına göre farklılık genel olarak değişse de amaç; hatalara neden olan new, delete operatörleriyle açıktan açığa bellek tahsisini kodumuzda yapmamak.

auto_ptr < T > (Deprecated):
C++ 98 standartlarıyla beraber dile katılan şablon sınıfı auto_ptr temel olarak bellek alanlarının tahsisi ve geri iadesi üzerine odaklandı. Fakat yanlış kullanımı nedeniyle memory hatalarından kaçalım diye tasarlanan akıllı göstericilerin amacının tam tersi şekilde memory hatasına neden oldu. Aşağıdaki kodu inceleyelim.

#include <iostream>
#include <memory>

using namespace std;

struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(auto_ptr<s> e)
{
    e->a = 99;
}

int main() 
{    
    auto_ptr<s> aptr(new s);
    
    cout  << "func begin" << endl;    
    func(aptr);
    cout  << "func end" << endl;      
    
    cout << aptr->a << endl;
     
}

Örnek kodda aptr nesnesi ile ‘f’ fonksiyonu çağrıldğında copy ctor çağırılacak ve aynı nesneyi gösteren bir diğer auto_ptr nesnesi oluşturulacak. Ve bu nesnenin(e) ömrü fonksiyon blokları sonlandığında sonlanacağı için gösterdiği dinamik bellek alanını da free edecek. Dolayısıyla aptr nesnesi artık free edilmiş bir bellek alanını göstermektedir kısacası dangling pointer hatasına sebep olur. Program seg fault vererek aşağıdaki şekilde sonlanır:

S ctor
func begin
~S dtor
func end

RUN FINISHED; Segmentation fault; core dumped; real time: 90ms; user: 0ms; system: 0ms

auto_ptr sınıfı C++17 standartlarıyla beraber standart kütüphaneden çıkarılacaktır. Dolayısıyla kullanmayın .)

unique_ptr < T >
auto_ptr nin yaptığı hatayı yapmaz, tek bir sahiplik vardır. Copy ctor, operator = fonksiyonlarını =delete şeklinde yasaklar dolayısıyla kullanıcı herhangi bir yerde herhangi bir şekilde nesneyi kopyalamaya çalıştığında derleme hatası alacaktır.

Yukarıdaki örneği unique_ptr ile yeniden yazarsak derleme zamanında aşağıdaki gibi bir hata alırız:

main.cpp:29:14: error: use of deleted function ‘std::unique_ptr&amp;amp;amp;lt;_Tp, _Dp&amp;amp;amp;gt;::unique_ptr(const std::unique_ptr&amp;amp;amp;lt;_Tp, _Dp&amp;amp;amp;gt;&amp;amp;amp;amp;) [with _Tp = s; _Dp = std::default_delete&amp;amp;amp;lt;s&amp;amp;amp;gt;]’

Yani ‘deleted’ bir fonksiyon olan copy ctorı çağıramazsın. Peki bir unique_ptr yi bir fonksiyona veya bir sınıfa nasıl transfer edeceğiz, call by value yerine call by reference ile, tekrardan üstteki örnek:

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(unique_ptr<s> &e)
{
    e->a = 99;
}

int main() 
{    
    unique_ptr<s> uptr(new s);
    
    cout  << "func begin" << endl;    
    func(uptr);
    cout  << "func end" << endl;      
    
    cout << uptr->a << endl;
     
}

Yapıtığım tek şey ‘void func(unique_ptr)’ fonksiyon imzasını ‘void func(unique_ptr &)’ ile değiştirmek böylelikle sahiplik değişmeden unique_ptr sınıfını func fonksiyonuna parametrik olarak geçirmiş oluruz.Ayrıca std::move kullanarak da sahipliğini ‘move’ ederek değiştirebiliriz. En nihayetinde auto_ptr de karşılaştığımız bellek(memory) sorunlarından da kurtuluyoruz.

shared_ptr< T >
Her ne kadar unique_ptr ile bellek hatalarını izole etsekte kullanım olarak hala sıkıntılarımız mevcut. Ham (raw) göstericilerdeki özellikleri olabildiğince gerçek hale getirmek akıllı göstericilerin implementasyonuna yön veren amaçlardan biri. Öyleyse bir den fazla pointerın aynı bellek alanına işaret etmesini de gerçeklemek gerekiyor.

Tam bu nokta da shared_ptr devreye giriyor. shared_ptr aslında bir referans sayacı tutuyor. shared_ptr ctor’ında başlangıç değeri olarak ‘new’ ile oluşturulan nesne verildiğinde aslında iki tane bellek tahsisi mevcut. Birincisi direkt zaten ‘new’ ile tahsis edilen nesne ikincisi ise referans sayacını kontrol eden, yöneten nesne(Manager Object). Örneğin sp1 ilk oluşturulan nesnemiz olsun içinde bir de Manager Object oluşturulacak. Diğer oluşturulan shared_ptr nesneleri için bu nesne de bulunan sayaç bir arttırılacak. Tam tersi şekilde Manager object içindeki shared sayacı ve weak sayacı sıfırlandığında ise heap bellek alanından tahsis edilen nesne free edilecek.

Yine aynı örnek üzerinden gidersek:

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(shared_ptr<s> e)
{
    e->a = 99;
    cout << "count of shared pointer:" << e.use_count() << endl;
}

int main() 
{    
    shared_ptr<s> sptr(new s);
    
    cout  << "func begin" << endl;    
    func(sptr);
    cout  << "func end" << endl;      
    
    cout << sptr->a << endl;
     
}

Görüldüğü üzere func fonksiyonunu referansla çağırmama rağmen herhangi bir bellek hatasıyla karşılaşmıyorum. Burda Manager object, shared pointer sayacını bir arttırıyor. Func fonksiyonu sonunda ise yine sayacı bir azaltıyor(Argüman olarak yaratılan ‘shared_ptr’ türünden ‘e’ nesnesinin ömrü sona erdiği için). Sonrasında process sorunsuz olarak sonlanıyor.

make_shared< T >: Yukarıda bahsettiğimizi gibi shared pointer oluşturulduğunca 2 kez dinamik bellek tahsisi yapılır. Birisi yönetici nesne için diğeri ise T türü için. Performans açısından bu ikisini aynı anda tahsis etmek maliyetli bir işlemdir. Bu soruna standart kütüphaneden make_shared< T > ile çözüm bulunmuş. make_shared< T > kullanıldığında aynı bir kez bellek tahsisini hem T türü için hem de yönetici nesne için beraber yapar. Yani tek bir blok heapten alınır bu da tasarımı itibarıyla pahalı işlem olan dinamik bellek tahsisi teke düşmüş olur. Yine yukarıdaki örnekten devam edersek:

//shared_ptr<s> sptr(new s);
shared_ptr<s> sptr = make_shared<s>();

şeklinde değiştirmek yeterli olacaktır.

weak_ptr < T >
Raw pointer aracılığıyla oluşturulamaz shared_ptr veya başka bir weak_ptr nin kopyalanması, atanmasıyla yaratılır. shared_ptr < T > gibi çalışır ama nesnenin ömrüne müdahalede bulunmaz. Yönetici nesnede (Manager Object) sayaç sıfır dahi olsa hiçbir şekilde tahsis edilen objenin ömrüne etki de bulunmaz. Sadece okuma hakkında sahiptir. Ownership (sahiplik) sadece shared_ptr dedir. Tipik olarak nesneye erişiminizin gerektiği fakat sayacı artırmanıza gerek olmadığı durumlarda kullanılır. Ayrıca lock() fonksiyonu ile weak_ptr < T > işaret ettiği shared_ptr elde edilebilir.

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(shared_ptr<s> e)
{
    e->a = 99;
    cout << "count of shared pointer:" << e.use_count() << endl;
}

int main() 
{    
    shared_ptr<s> sptr = make_shared<s>();
    {
        //weak_ptr<s> wptr (new s);   error: main.cpp:29:33: error: no matching function for call to ‘std::weak_ptr<s>::weak_ptr(s*)’
        weak_ptr<s> wptr = sptr;
        cout << "count of weak pointer:" << wptr.use_count() << endl;
    }

    
    cout  << "func begin" << endl;    
    func(sptr);
    cout  << "func end" << endl;      
    
    cout << sptr->a << endl;
     
}

enable_shared_from_this < T >: weak_ptr < T > ‘ nin kullanıldığı yerlerden biridir. Boost kütüphanesinin C++ diline kazandırdığı bir diğer özellikdir. Örneğin bir üye fonksiyondan başka bir fonksiyona ‘this’ pointerını geçmek istediğinizde eğer nesne shared_ptr tarafından kontrol ediliyorsa bir diğer shared_ptr objesi yaratmak durumunda kalacaksınız. Bu da yine dangling pointer ‘a sebep olacak. Örneğin:

void printb (struct s *ps)
{
    cout << ps->b << endl;
}


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    printB() 
    {
        printb(this);
        
    }
};

bu kodu shared_ptr ile gerçeklediğimiz düşünelim


void printb (shared_ptr<s> ps)
{
    cout << ps->b << endl;
}


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    printB() 
    {
        shared_ptr<s> t(this); // !hata: tehlike aynı objeye iki Manager Object oluşturuldu.
        printb(s);
        
    }
};

Burda görüldüğü üzere aynı bellek alanını iki adet manager object yönettiğinde herhangi birinin sayacı sıfırlandığında gösterdiği obje free edileceğinden yine bir dangling pointer olma durumu mevcut. Bunu engellemek için sınıf içerisinde bir tane weak_ptr < T > barındırarak nesnenin ömrüne etki etmiyoruz. Bunu da türetme yöntemiyle şu şekilde implemente ediyoruz:

void printb (shared_ptr<s> ps);
struct s : public enable_shared_from_this<s>
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    void printB() 
    {
        shared_ptr<s> sptr = shared_from_this(); // burda ikinci bir manager object üretilmiyor, sadece shared_ptr counter 1 arttırılıyor.
        printb(sptr);
        
    }
};

void printb (shared_ptr<s> ps)
{
    cout << ps->b << endl;
}

Her ne kadar smart pointers size daha güvenli bir kullanım sunsa da yine de dikkat edilmesi gereken yerler var. Özellikle unique_ptr, shared_ptr ve weak_ptr şablon sınıflarının kullanımlarının iyi bir şekilde öğrenilmesi ve uygulamayla pratik yapılmasında fayda var. He bir de auto_ptr ‘ yi aklınıza bile getirmeyin 🙂