C++ Dilinde Smart Pointers - "std::shared_ptr"
- Yusuf Hançar
- Sep 23, 2023
- 5 min read
Önceki yazımızda C++ dilindeki dinamik bellek yönetimi, nesne oluşturma ve bu nesnenin hayatını kontrol etme mekanizmalarından bahsettik. Modern C++ ile dile eklenen akıllı işaretçilerin(smart pointers) RAII idiomu sayesinde kaynak ve memory sızıntılarının önüne geçmesine katkısından ve kullanım senaryolarından bahsetmiştik. İlk olarak değerlendirdiğimiz std::unique_ptr tek sahiplik ilkesiyle kaynak yönetimini template parametresi Deleter kullanarak otomatik bellek yönetimini sağlamaktadır.
Bu yazımızda diğer bir akıllı işaretçi olan std::shared_ptr sınıfını inceleyeceğiz. Yine unique_ptr gibi arka planda nasıl çalıştığını inceleyerek başlayalım.
Bu defa inceleyeceğimiz shared_ptr akıllı işaretçisi, unique_ptr aksine bellek alanının(heap) birden fazla sahibinin olabileceği bir yapı sunar. Tabiki birden fazla sahibi olabilir ancak bellek sızıntısı ve kaynak sızıntısına mahal vermediğini unutmuyoruz. Bunu da bellek alanına sahip olan son shared_ptr nesnesi yapmaktadır. Yani en son kaynağı gösteren pointer'ın hayatı bittiğinde kaynak tamamen sonladırılmaktadır. Bir nesne birden fazla veri yapısında tutulabilmektedir. Bu durumda o nesneye birden fazla yerde referans almak gerekecektir ve hepsi aynı nesneye işaret edecektir. Bu nesne dinamik bellek alanında oluşturulmuş ve konumdaki gibi akıllı işaretçi olmasaydı kimin delete edeceğini bilmemiz oldukça zor olurdu.
unique_ptr gibi memory başlık dosyasında yer almaktadır ancak kopyalamaya karşı açıktır. Yani copy members delete edilmemiştir.
shared_ptr sınıfına ait bir nesne oluşturulduğunda içerisinde iki adet pointer tutacaktır. İşaretçilerden biri dinamik olarak oluşturulan nesnenin adresini tutarken, diğeri pointer ise oluşturulan kontrol bloğunun silinmesinden sorumludur. Diğer alanında işaretçi ile tutulma nedeni o alanın da dinamik olarak oluşturulmasıdır. Template tür parametresi olarak deleter bulundurmamaktadır.
#include <memory>
template <class T>
class shared_ptr;

Yukarıdaki resimde bahsettiğimiz iki pointer ve kontrol ettikleri yerler gösterilmektedir. Sonraki resimde de biraz daha ayrıntılı işaretçiler, oluşturulan bloklar ve nesnenin oluşturulması süreci izlenebilmektedir. Şimdi bu kavramları biraz daha derinlemesine inceleyerek örneklendirelim.

std::shared_ptr ve reference count ilişkisi
En önemli özelliklerinden olan birden fazla nesnenin aynı kaynağı kullanabilmesidir. Bunu yaparken oluşturulan 'Control Block' alanında sahip olan tüm işaretçiler için bu counter kullanılmaktadır. Paylaşılan kaynağın ne kadar süre ve güvenli şekilde hayatta kalması, işi bittiğinde bellek alanının geri verilmesini bu sayede kontrol edebilmektedir. Sahip olan her işaretçi için bu sayaç bir artırılmaktadır. Aynı şekilde sahipliği bırakan her bir pointer için de bu sayaç azaltılacak ve sahipliği tutan son işaretçi ile counter değeri sıfırlanacak ve bellek alanı geri verilecektir.
std::shared_ptr ve use_count() ilişkisi
long use_count() const noexcept;
*****************************
AÇIKLAMA: kontrol edilen shared_ptr instance kaç tane ise onu döndürür.
*****************************
CEVAP:
*****************************
Kaynağa işaret eden kaç nesnenin olduğunu geri dönen fonksiyondur. Yönetilen herhangi bir nesne yoksa fonksiyon sıfır döndürecektir. Genel olarak sıfır olup olmadığı kontrol edilerek paylaşılan işaretçinin boş olduğu ve hiçbir nesneyi yönetmediğini öğrenmek için kullanılır. Fonksiyon dönüş değeri 1 ise tek bir sahibi olduğu anlaşılır.
C++17 ye kadar "unique()" fonksiyonu tek sahibi olup olmadığını kontrol etmek için kullanılmaktaydı. C++17 standartları ile deprecated, C++20 standartları ile dilden tamamen kaldırıldı. "bool unique() const noexcept;"
#include <iostream>
#include <memory>
using namespace std;
class SmartCode {
public:
SmartCode(int val1, int val2) : a(val1), b(val2) {}
void show_values()
{
cout << "a : " << a << ", b : " << b << endl;
}
private:
int a, b;
};
int main()
{
std::shared_ptr<SmartCode> sp1{ new SmartCode{ 12, 24 } };
std::cout << "use_count : " << sp1.use_count() << std::endl;
auto sp2 = sp1;
auto sp3 = sp2;
auto sp4 = sp3;
auto sp5 = sp4;
std::cout << "use_count : " << sp5.use_count() << std::endl;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP:
use_count : 1
use_count : 5
*****************************
#include <iostream>
#include <memory>
using namespace std;
class SmartCode {
public:
SmartCode(int val1, int val2) : a(val1), b(val2) {}
void show_values()
{
cout << "a : " << a << ", b : " << b << endl;
}
private:
int a, b;
};
int main ()
{
auto sp = std::make_shared<SmartCode>(1, 3);
// auto sp1 = sp;
if (sp.unique())
{
cout << "last pointer" << endl;
}
else
{
cout << "none" << endl;
}
}
*****************************
AÇIKLAMA: yorum satırındaki kodu açarsak eğer birden fazla sahiplik olacağı için none olacaktır. unique() işlevi son işaretçi olup olmadığını döndürür. Yani *this nesnesinin son pointer olup olmadığını döndürür. (use_count() == 1 durumunu ?)
*****************************
CEVAP: last pointer
*****************************
std::shared_ptr ve std::make_shared() ilişkisi
std::unique_ptr sınıfının make_unique kullanarak dinamik bellek yönetimini otomatize etmesi ve memory leak riskini azaltması gibi std::make_shared aynı görevi üstlenmektedir. Ayrıca yukarıda bahsettiğimiz iki ayrı dinamik bellek bloğu(biri sınıf nesnesi, diğeri kontrol blok için) oluşturmak yerine tek bir bellek bloğu oluşturulmasını sağlamaktadır. Bu faydası da run-time maliyetini azaltmasını sağlar.
template<class T, class... Args>
shared_ptr<T> make_shared( Args&&... args);
*****************************
AÇIKLAMA: T türünde oluşturulan nesne ve kurucu işlevi için tüm parametreleri args kullanılarak std::shared_ptr içine sarmalamaktadır. ::new(pv) T(std::forward<Args>(args)...) şeklinde nesne oluşturulduğu düşünülebilir.
pv nesneyi depolamaya yönelik bir void* işaretçisidir. std::share_ptr sınıfının kurucu işlevi ile oluşturulan nesne için pointer ile paylaşılan shared_from_this etkinleştirilir.
*****************************

Hangi durumlarda control blok oluşturulur ?
shared_ptr nesnesi doğrudan adresle hayata getirilirse
Dinamik ömürlü nesnenin adresi ile hayata getirilirse
#include <iostream>
#include <memory>
using namespace std;
class SmartCode {
public:
SmartCode(int val1, int val2) : a(val1), b(val2) {}
void show_values()
{
cout << "a : " << a << ", b : " << b << endl;
}
private:
int a, b;
};
int main ()
{
std::shared_ptr<SmartCode>sptr(new SmartCode{ 1, 10 });
}
*****************************
AÇIKLAMA: dinamik ömürlü nesneyi gösteren ilk shared_ptr nesnesi olduğundan control blok oluşturulması garanti edilmiştir.
*****************************
CEVAP:
*****************************
int main ()
{
auto uptr = make_unique<SmartCode>(1, 3); // control blok yok. çünkü unique_ptr
shared_ptr<SmartCode>sptr(std::move(uptr)); // burada control blok oluşur.
}
*****************************
AÇIKLAMA:
*****************************
CEVAP:
*****************************
std::shared_ptr ve get() ilişkisi
T* get()const noexcept; // C++17 standartlarına kadar
element_type* get()const noexcept; // C++ 17 ve sonrası...
#include <iostream>
#include <memory>
#include <string>
int main()
{
std::string* ptr{};
std::shared_ptr<std::string> s_int = std::make_shared<std::string>("smartcode");
ptr = s_int.get();
std::cout << "s_int access to : " << *ptr << std::endl;
return 0;
}
*****************************
AÇIKLAMA: get() ile işaret edilen nesneye ait işaretçi elde edilir. shared_ptr nesnesinin ömrü tamamlandığında ptr geçersiz olacaktır, bu duruma dikkat edilmelidir.
*****************************
CEVAP:
*****************************
#include <iostream>
#include <memory>
#include <string>
int main()
{
std::string* ptr{};
{
shared_ptr<string> s_int = make_shared<string>("smartcode");
ptr = s_int.get();
}
std::cout << "s_int access to : " << *ptr << std::endl;
return 0;
}
*****************************
AÇIKLAMA: bu durum tanımsız davranıştır. Yukarıdaki açıklamadan dolayı dikkat edilmelidir.
*****************************
CEVAP:
*****************************
std::shared_ptr ve tip dönüşüm işlemleri ilişkisi
std::static_pointer_cast
std::dynamic_pointer_cast
std::const_pointer_cast
std::reinterpret_pointer_cast (güvenli olmayan cast!!!)
#include <iostream>
#include <memory>
class Base {
public:
virtual void v_func()
{
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void v_func() override
{
std::cout << "Derived" << std::endl;
}
};
int main()
{
std::shared_ptr<Base> bptr = std::make_shared<Derived>();
std::shared_ptr<Derived> dptr = std::static_pointer_cast<Derived>(bptr);
dptr->v_func();
return 0;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: Derived
*****************************
#include <iostream>
#include <memory>
class Base {
public:
virtual void v_func()
{
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void v_func() override
{
std::cout << "Derived" << std::endl;
}
};
int main()
{
std::shared_ptr<Base> bptr = std::make_shared<Derived>();
std::shared_ptr<Derived> dptr = std::dynamic_pointer_cast<Derived>(bptr);
if (dptr)
{
dptr->show();
}
else
{
std::cout << "Failed conversion." << std::endl;
}
return 0;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: Derived
*****************************
i#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<const int> cptr = std::make_shared<const int>(23);
std::shared_ptr<int> iptr = std::const_pointer_cast<int>(cptr);
std::cout << *iptr << std::endl;
return 0;
}
*****************************
AÇIKLAMA:
*****************************
CEVAP: 23
*****************************

Comments