C++

Спільне володіння: std::shared_ptr та std::weak_ptr

Підрахунок посилань, циклічні залежності, блок управління та слабкі вказівники. Розумні покажчики для складних графів об\

Спільне володіння: std::shared_ptr та std::weak_ptr

Обмеження виключного володіння

У попередній статті ми розглянули std::unique_ptr — розумний покажчик з виключним володінням. Однак у реальних програмах часто виникають ситуації, коли кілька частин програми мають спільно володіти одним ресурсом:

SharedOwnershipProblem.cpp
#include <iostream>
#include <memory>
#include <string>

class Document
{
private:
    std::string m_name;

public:
    Document(const std::string& name) : m_name(name)
    {
        std::cout << "Document '" << m_name << "' opened\n";
    }

    ~Document()
    {
        std::cout << "Document '" << m_name << "' closed\n";
    }

    void display() const
    {
        std::cout << "Displaying: " << m_name << "\n";
    }
};

class Editor
{
private:
    std::unique_ptr<Document> m_document;

public:
    void openDocument(std::unique_ptr<Document> doc)
    {
        m_document = std::move(doc);
    }

    void display() const
    {
        if (m_document)
            m_document->display();
    }
};

class Viewer
{
private:
    std::unique_ptr<Document> m_document;

public:
    void setDocument(std::unique_ptr<Document> doc)
    {
        m_document = std::move(doc);
    }

    void display() const
    {
        if (m_document)
            m_document->display();
    }
};

int main()
{
    auto doc = std::make_unique<Document>("Report.txt");
    
    Editor editor;
    Viewer viewer;
    
    editor.openDocument(std::move(doc));
    // ❌ Проблема: doc тепер nullptr, не можемо передати viewer!
    // viewer.setDocument(std::move(doc));  // Помилка: doc вже порожній
    
    return 0;
}
./SharedOwnershipProblem
Document 'Report.txt' opened
Document 'Report.txt' closed
Проблема виключного володіння:std::unique_ptr не може бути спільно використаний — після std::move() початковий вказівник стає nullptr.Для спільного володіння потрібен інший механізм — std::shared_ptr.

std::shared_ptr: семантика спільного володіння

std::shared_ptr<T> — розумний покажчик з семантикою спільного володіння (shared ownership):

  • Кілька std::shared_ptr можуть володіти одним ресурсом одночасно
  • Підрахунок посилань (reference counting): відстежує кількість власників
  • Автоматичне видалення: ресурс знищується, коли останній shared_ptr виходить з scope
  • Копіювання дозволене: створення копії збільшує лічильник посилань
SharedPtrBasics.cpp
#include <iostream>
#include <memory>

class Resource
{
private:
    int m_id;

public:
    Resource(int id) : m_id(id)
    {
        std::cout << "Resource " << m_id << " acquired\n";
    }

    ~Resource()
    {
        std::cout << "Resource " << m_id << " destroyed\n";
    }

    int getId() const { return m_id; }
};

int main()
{
    std::shared_ptr<Resource> ptr1;
    
    {
        // Створюємо перший shared_ptr
        ptr1 = std::make_shared<Resource>(1);
        std::cout << "Reference count: " << ptr1.use_count() << "\n";
        
        {
            // Створюємо другий shared_ptr через копіювання
            std::shared_ptr<Resource> ptr2 = ptr1;
            std::cout << "Reference count: " << ptr1.use_count() << "\n";
            
            {
                // Створюємо третій shared_ptr
                std::shared_ptr<Resource> ptr3 = ptr2;
                std::cout << "Reference count: " << ptr1.use_count() << "\n";
                
                std::cout << "Exiting inner scope...\n";
            }  // ptr3 знищується, лічильник: 3 → 2
            
            std::cout << "Reference count: " << ptr1.use_count() << "\n";
            std::cout << "Exiting middle scope...\n";
        }  // ptr2 знищується, лічильник: 2 → 1
        
        std::cout << "Reference count: " << ptr1.use_count() << "\n";
        std::cout << "Exiting outer scope...\n";
    }  // ptr1 знищується, лічильник: 1 → 0, ресурс видаляється
    
    std::cout << "Program ends\n";
    
    return 0;
}
./SharedPtrBasics
Resource 1 acquired
Reference count: 1
Reference count: 2
Reference count: 3
Exiting inner scope...
Reference count: 2
Exiting middle scope...
Reference count: 1
Exiting outer scope...
Resource 1 destroyed ← Видаляється лише коли лічильник = 0
Program ends
Loading diagram...
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "JetBrains Mono"
skinparam defaultFontSize 13

!define SHARED_COLOR #10b981
!define CONTROL_COLOR #f59e0b

rectangle "Control Block" as CB #f59e0b {
    map "Блок управління" as Control {
        Лічильник посилань => 3
        Слабких посилань => 0
        Вказівник на об'єкт => 0x1A2B3C
        Делетер => default_delete
    }
}

rectangle "Resource" as R #3b82f6 {
    class "Resource(id=1)" as Res {
        m_id = 1
    }
}

actor "shared_ptr ptr1" as P1 #10b981
actor "shared_ptr ptr2" as P2 #10b981
actor "shared_ptr ptr3" as P3 #10b981

P1 -right-> CB : володіє
P2 -right-> CB : володіє
P3 -right-> CB : володіє

CB -down-> R : керує

note right of CB
  Коли лічильник = 0:
  → викликає делетер
  → знищує Resource
end note

@enduml

Фабрична функція std::make_shared

Як і з unique_ptr, для створення shared_ptr рекомендується використовувати std::make_shared<T>():

MakeShared.cpp
#include <iostream>
#include <memory>

class Widget
{
private:
    int m_value;

public:
    Widget(int value) : m_value(value)
    {
        std::cout << "Widget(" << m_value << ") created\n";
    }

    ~Widget()
    {
        std::cout << "Widget(" << m_value << ") destroyed\n";
    }

    int getValue() const { return m_value; }
};

int main()
{
    // ❌ Старий спосіб (не рекомендується)
    std::shared_ptr<Widget> widget1(new Widget(10));
    
    // ✅ Сучасний спосіб (рекомендується)
    auto widget2 = std::make_shared<Widget>(20);
    
    std::cout << "widget1: " << widget1->getValue() << "\n";
    std::cout << "widget2: " << widget2->getValue() << "\n";
    
    return 0;
}
./MakeShared
Widget(10) created
Widget(20) created
widget1: 10
widget2: 20
Widget(20) destroyed
Widget(10) destroyed

Переваги std::make_shared

1️⃣ Продуктивність

// ❌ Дві алокації:
shared_ptr<Widget> p(new Widget(10));
// 1. new Widget(10) — виділення для об'єкта
// 2. Control Block — виділення для лічильника

// ✅ Одна алокація:
auto p = make_shared<Widget>(10);
// Одне виділення для об'єкта + Control Block разом

2️⃣ Безпека винятків

// ❌ Небезпечно:
processWidgets(
    shared_ptr<Widget>(new Widget(1)),
    mayThrow()
);
// Якщо mayThrow() викине виняток після new,
// але до створення shared_ptr → витік!

// ✅ Безпечно:
processWidgets(
    make_shared<Widget>(1),
    mayThrow()
);

3️⃣ Лаконічність

// ❌ Багатослівно
std::shared_ptr<VeryLongClassName> p(
    new VeryLongClassName(args)
);

// ✅ Компактно з auto
auto p = std::make_shared<
    VeryLongClassName
>(args);

4️⃣ Узгодженість

Однакова форма створення:

  • make_unique<T>()
  • make_shared<T>()
  • Легко перейти між ними
Правило: Завжди використовуйте std::make_shared<T>() замість new при створенні std::shared_ptr.

Інтерфейс std::shared_ptr

std::shared_ptr має схожий інтерфейс з std::unique_ptr, але з додатковими методами для роботи з лічильником:

SharedPtrInterface.cpp
#include <iostream>
#include <memory>

class Data
{
private:
    int m_value;

public:
    Data(int value) : m_value(value)
    {
        std::cout << "Data(" << m_value << ") created\n";
    }

    ~Data()
    {
        std::cout << "Data(" << m_value << ") destroyed\n";
    }

    void setValue(int value) { m_value = value; }
    int getValue() const { return m_value; }
};

int main()
{
    // Створення
    auto ptr1 = std::make_shared<Data>(42);
    
    // Оператори * та ->
    std::cout << "Value: " << ptr1->getValue() << "\n";
    (*ptr1).setValue(100);
    std::cout << "New value: " << ptr1->getValue() << "\n";
    
    // use_count() — кількість власників
    std::cout << "Owners: " << ptr1.use_count() << "\n";
    
    // Копіювання збільшує лічильник
    auto ptr2 = ptr1;
    std::cout << "Owners after copy: " << ptr1.use_count() << "\n";
    
    // get() — сирий вказівник
    Data* raw = ptr1.get();
    std::cout << "Raw pointer value: " << raw->getValue() << "\n";
    
    // reset() — замінює ресурс (зменшує лічильник)
    ptr2.reset();
    std::cout << "Owners after reset: " << ptr1.use_count() << "\n";
    
    // unique() — чи є єдиним власником?
    std::cout << "Is unique: " << std::boolalpha << ptr1.unique() << "\n";
    
    // Перевірка на nullptr
    if (ptr1)
        std::cout << "ptr1 is not null\n";
    
    if (!ptr2)
        std::cout << "ptr2 is null\n";
    
    return 0;
}
./SharedPtrInterface
Data(42) created
Value: 42
New value: 100
Owners: 1
Owners after copy: 2
Raw pointer value: 100
Owners after reset: 1
Is unique: true
ptr1 is not null
ptr2 is null
Data(100) destroyed

Основні методи std::shared_ptr

МетодОписПриклад
use_count()Повертає кількість власниківptr.use_count() == 3
unique()Чи є єдиним власником?ptr.unique() == true
get()Повертає сирий вказівникData* raw = ptr.get();
reset()Замінює ресурс або робить nullptrptr.reset(new Data(5));
operator*Розіменування(*ptr).setValue(10);
operator->Доступ до членівptr->setValue(10);
operator boolПеревірка на nullptrif (ptr) { ... }

Блок управління (Control Block)

std::shared_ptr зберігає два вказівники всередині:

  1. Вказівник на об'єкт — керований ресурс
  2. Вказівник на блок управління — метадані для підрахунку посилань

Структура блоку управління

ControlBlockStructure.cpp
// Спрощена схема блоку управління (псевдокод)
struct ControlBlock
{
    size_t refCount;        // Кількість shared_ptr
    size_t weakCount;       // Кількість weak_ptr
    void* resourcePtr;      // Вказівник на ресурс
    Deleter deleter;        // Функція видалення
    Allocator allocator;    // Алокатор
};
Loading diagram...
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "JetBrains Mono"
skinparam defaultFontSize 12

package "shared_ptr (16 bytes)" {
    rectangle "ptr_to_object" as PO #3b82f6
    rectangle "ptr_to_control" as PC #f59e0b
}

package "Control Block" {
    map "Блок управління" as CB {
        refCount => 2
        weakCount => 1
        resourcePtr => 0x...
        deleter => ...
    }
}

package "Heap Memory" {
    rectangle "Managed Object" as MO #10b981 {
        class Widget {
            m_value
        }
    }
}

PO -down-> MO : вказує на
PC -down-> CB : вказує на
CB -down-> MO : керує

note right of CB
  Коли refCount = 0:
  → викликає deleter(resourcePtr)
  
  Коли weakCount = 0:
  → знищує Control Block
end note

@enduml
Важливо:sizeof(shared_ptr<T>) = 16 байт (на 64-бітних системах):
  • 8 байт — вказівник на об'єкт
  • 8 байт — вказівник на блок управління
Це вдвічі більше за unique_ptr (8 байт), але все одно компактно.

Копіювання та переміщення std::shared_ptr

На відміну від unique_ptr, std::shared_ptr підтримує копіювання:

SharedPtrCopyMove.cpp
#include <iostream>
#include <memory>
#include <utility>

class Resource
{
private:
    int m_id;

public:
    Resource(int id) : m_id(id)
    {
        std::cout << "Resource " << m_id << " created\n";
    }

    ~Resource()
    {
        std::cout << "Resource " << m_id << " destroyed\n";
    }

    int getId() const { return m_id; }
};

int main()
{
    auto ptr1 = std::make_shared<Resource>(1);
    std::cout << "ptr1 count: " << ptr1.use_count() << "\n";
    
    // ✅ Копіювання — збільшує лічильник
    auto ptr2 = ptr1;
    std::cout << "After copy:\n";
    std::cout << "  ptr1 count: " << ptr1.use_count() << "\n";
    std::cout << "  ptr2 count: " << ptr2.use_count() << "\n";
    
    // ✅ Переміщення — НЕ збільшує лічильник
    auto ptr3 = std::move(ptr1);
    std::cout << "After move:\n";
    std::cout << "  ptr1 is " << (ptr1 ? "not null" : "null") << "\n";
    std::cout << "  ptr3 count: " << ptr3.use_count() << "\n";
    
    return 0;
}
./SharedPtrCopyMove
Resource 1 created
ptr1 count: 1
After copy:
ptr1 count: 2
ptr2 count: 2
After move:
ptr1 is null
ptr3 count: 2
Resource 1 destroyed
Копіювання vs Переміщення
Filter
ОпераціяЛічильникptr1ptr2ptr3
Початок1✅ (володіє)——
Після copy2 ↑✅ (володіє)✅ (володіє)—
Після move2 (без змін)❌ (nullptr)✅ (володіє)✅ (володіє)
Running
Process: 12842
Копіювання shared_ptr:
  • Збільшує refCount
  • Обидва вказівники залишаються валідними
  • Атомарна операція (безпечна для багатопоточності)
Переміщення shared_ptr:
  • НЕ змінює refCount
  • Вихідний вказівник стає nullptr
  • Швидше за копіювання (не потрібно змінювати лічильник)

Розв'язання проблеми спільного володіння

Тепер можемо переписати приклад з початку статті, використовуючи std::shared_ptr:

SharedOwnershipSolution.cpp
#include <iostream>
#include <memory>
#include <string>

class Document
{
private:
    std::string m_name;

public:
    Document(const std::string& name) : m_name(name)
    {
        std::cout << "Document '" << m_name << "' opened\n";
    }

    ~Document()
    {
        std::cout << "Document '" << m_name << "' closed\n";
    }

    void display() const
    {
        std::cout << "Displaying: " << m_name << "\n";
    }
};

class Editor
{
private:
    std::shared_ptr<Document> m_document;

public:
    void openDocument(std::shared_ptr<Document> doc)
    {
        m_document = doc;  // ✅ Копіювання дозволене
    }

    void display() const
    {
        if (m_document)
            m_document->display();
    }
};

class Viewer
{
private:
    std::shared_ptr<Document> m_document;

public:
    void setDocument(std::shared_ptr<Document> doc)
    {
        m_document = doc;  // ✅ Копіювання дозволене
    }

    void display() const
    {
        if (m_document)
            m_document->display();
    }
};

int main()
{
    auto doc = std::make_shared<Document>("Report.txt");
    std::cout << "Reference count: " << doc.use_count() << "\n\n";
    
    Editor editor;
    Viewer viewer;
    
    editor.openDocument(doc);  // ✅ Працює! Лічильник: 1 → 2
    std::cout << "After editor: " << doc.use_count() << "\n";
    
    viewer.setDocument(doc);   // ✅ Працює! Лічильник: 2 → 3
    std::cout << "After viewer: " << doc.use_count() << "\n\n";
    
    editor.display();
    viewer.display();
    
    std::cout << "\nProgram ends...\n";
    
    return 0;
}  // Document знищується, коли останній shared_ptr виходить з scope
./SharedOwnershipSolution
Document 'Report.txt' opened
Reference count: 1
After editor: 2
After viewer: 3
Displaying: Report.txt
Displaying: Report.txt
Program ends...
Document 'Report.txt' closed ← Коректно знищується

Небезпека циклічних залежностей

std::shared_ptr має фундаментальну проблему — циклічні залежності (circular references), що призводять до витоків пам'яті:

CircularDependency.cpp
#include <iostream>
#include <memory>
#include <string>

class Person;

class Person
{
private:
    std::string m_name;
    std::shared_ptr<Person> m_partner;  // ← Проблема!

public:
    Person(const std::string& name) : m_name(name)
    {
        std::cout << m_name << " created\n";
    }

    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    void setPartner(std::shared_ptr<Person> partner)
    {
        m_partner = partner;
    }

    const std::string& getName() const { return m_name; }
};

int main()
{
    {
        auto alice = std::make_shared<Person>("Alice");
        auto bob = std::make_shared<Person>("Bob");
        
        // Створюємо циклічну залежність
        alice->setPartner(bob);   // alice → bob (refCount bob: 1 → 2)
        bob->setPartner(alice);   // bob → alice (refCount alice: 1 → 2)
        
        std::cout << "\nExiting scope...\n";
    }  // alice і bob виходять з scope
    
    std::cout << "Program ends\n";
    
    return 0;
}
./CircularDependency
Alice created
Bob created
Exiting scope...
Program ends
← Alice і Bob НІКОЛИ не знищуються!
Loading diagram...
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "JetBrains Mono"
skinparam defaultFontSize 13

rectangle "alice (shared_ptr)" as A #3b82f6
rectangle "bob (shared_ptr)" as B #3b82f6

package "Person Alice" as PA #10b981 {
    map "Object" as OA {
        m_name => "Alice"
        m_partner => → Bob
    }
    map "Control Block" as CA {
        refCount => 2
    }
}

package "Person Bob" as PB #f59e0b {
    map "Object" as OB {
        m_name => "Bob"
        m_partner => → Alice
    }
    map "Control Block" as CB {
        refCount => 2
    }
}

A -down-> PA : володіє
B -down-> PB : володіє

OA -right-> PB : вказує (m_partner)
OB -left-> PA : вказує (m_partner)

note bottom of PA
  Після виходу alice з scope:
  refCount: 2 → 1 (залишився m_partner Bob)
  ❌ Об'єкт НЕ знищується
end note

note bottom of PB
  Після виходу bob з scope:
  refCount: 2 → 1 (залишився m_partner Alice)
  ❌ Об'єкт НЕ знищується
end note

@enduml
Циклічна залежність:При виході alice і bob з scope:
  1. alice виходить → refCount Alice: 2 → 1 (залишився m_partner Bob) → не знищується
  2. bob виходить → refCount Bob: 2 → 1 (залишився m_partner Alice) → не знищується
Результат: Alice не може знищити Bob, Bob не може знищити Alice → витік пам'яті!

std::weak_ptr: розв'язання циклічних залежностей

std::weak_ptr<T> — розумний покажчик-спостерігач (observer):

  • НЕ володіє ресурсом (не збільшує refCount)
  • Спостерігає за ресурсом, яким володіє shared_ptr
  • Безпечний: автоматично стає nullptr, якщо ресурс знищено
  • Конвертується в shared_ptr через метод lock()
WeakPtrBasics.cpp
#include <iostream>
#include <memory>

class Resource
{
private:
    int m_id;

public:
    Resource(int id) : m_id(id)
    {
        std::cout << "Resource " << m_id << " created\n";
    }

    ~Resource()
    {
        std::cout << "Resource " << m_id << " destroyed\n";
    }

    int getId() const { return m_id; }
};

int main()
{
    std::weak_ptr<Resource> weak;
    
    {
        auto shared = std::make_shared<Resource>(42);
        std::cout << "shared refCount: " << shared.use_count() << "\n";
        
        // Створюємо weak_ptr з shared_ptr
        weak = shared;
        std::cout << "After weak = shared:\n";
        std::cout << "  shared refCount: " << shared.use_count() << "\n";
        std::cout << "  weak expired: " << std::boolalpha << weak.expired() << "\n";
        
        // Використання weak_ptr: lock() → shared_ptr
        if (auto locked = weak.lock())
        {
            std::cout << "Locked! Resource ID: " << locked->getId() << "\n";
            std::cout << "  Temporary refCount: " << locked.use_count() << "\n";
        }
        
        std::cout << "\nExiting scope...\n";
    }  // shared знищується → Resource знищується
    
    std::cout << "\nAfter scope:\n";
    std::cout << "weak expired: " << weak.expired() << "\n";
    
    if (auto locked = weak.lock())
    {
        std::cout << "Resource still exists\n";
    }
    else
    {
        std::cout << "Resource was destroyed\n";
    }
    
    return 0;
}
./WeakPtrBasics
Resource 42 created
shared refCount: 1
After weak = shared:
shared refCount: 1 ← НЕ збільшився!
weak expired: false
Locked! Resource ID: 42
Temporary refCount: 2 ← Тимчасово збільшився через lock()
Exiting scope...
Resource 42 destroyed
After scope:
weak expired: true
Resource was destroyed

Основні методи std::weak_ptr

МетодОписПовертає
expired()Чи знищений ресурс?bool
lock()Створити shared_ptr (або nullptr)shared_ptr<T>
use_count()Кількість shared_ptr власниківsize_t
reset()Перестати спостерігатиvoid
Використання weak_ptr:
std::weak_ptr<Widget> weak = shared;

// ❌ Напряму НЕ можна використовувати:
// weak->doSomething();  // Помилка компіляції!

// ✅ Потрібна конвертація через lock():
if (auto temp = weak.lock())
{
    temp->doSomething();  // Працює!
}  // temp автоматично знищується

Розв'язання циклічної залежності з weak_ptr

Тепер виправимо приклад з циклічною залежністю:

CircularFixed.cpp
#include <iostream>
#include <memory>
#include <string>

class Person
{
private:
    std::string m_name;
    std::weak_ptr<Person> m_partner;  // ✅ Використовуємо weak_ptr!

public:
    Person(const std::string& name) : m_name(name)
    {
        std::cout << m_name << " created\n";
    }

    ~Person()
    {
        std::cout << m_name << " destroyed\n";
    }

    void setPartner(std::shared_ptr<Person> partner)
    {
        m_partner = partner;
    }

    void displayPartner() const
    {
        // Конвертуємо weak_ptr → shared_ptr для використання
        if (auto partner = m_partner.lock())
        {
            std::cout << m_name << "'s partner is " << partner->getName() << "\n";
        }
        else
        {
            std::cout << m_name << " has no partner\n";
        }
    }

    const std::string& getName() const { return m_name; }
};

int main()
{
    {
        auto alice = std::make_shared<Person>("Alice");
        auto bob = std::make_shared<Person>("Bob");
        
        std::cout << "alice refCount: " << alice.use_count() << "\n";
        std::cout << "bob refCount: " << bob.use_count() << "\n\n";
        
        // Встановлюємо партнерів (weak_ptr НЕ збільшує refCount)
        alice->setPartner(bob);   // alice → bob (refCount bob залишається 1)
        bob->setPartner(alice);   // bob → alice (refCount alice залишається 1)
        
        std::cout << "After setPartner:\n";
        std::cout << "alice refCount: " << alice.use_count() << "\n";
        std::cout << "bob refCount: " << bob.use_count() << "\n\n";
        
        alice->displayPartner();
        bob->displayPartner();
        
        std::cout << "\nExiting scope...\n";
    }  // alice і bob коректно знищуються!
    
    std::cout << "Program ends\n";
    
    return 0;
}
./CircularFixed
Alice created
Bob created
alice refCount: 1
bob refCount: 1
After setPartner:
alice refCount: 1 ← НЕ змінився!
bob refCount: 1 ← НЕ змінився!
Alice's partner is Bob
Bob's partner is Alice
Exiting scope...
Bob destroyed
Alice destroyed
Program ends
Loading diagram...
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "JetBrains Mono"
skinparam defaultFontSize 13

rectangle "alice (shared_ptr)" as A #3b82f6
rectangle "bob (shared_ptr)" as B #3b82f6

package "Person Alice" as PA #10b981 {
    map "Object" as OA {
        m_name => "Alice"
        m_partner => ⇢ Bob (weak)
    }
    map "Control Block" as CA {
        refCount => 1
        weakCount => 1
    }
}

package "Person Bob" as PB #f59e0b {
    map "Object" as OB {
        m_name => "Bob"
        m_partner => ⇢ Alice (weak)
    }
    map "Control Block" as CB {
        refCount => 1
        weakCount => 1
    }
}

A -down-> PA : володіє (strong)
B -down-> PB : володіє (strong)

OA ..> PB : спостерігає (weak)
OB ..> PA : спостерігає (weak)

note right of CA
  weak_ptr НЕ збільшує refCount
  → циклу володіння немає
  → об'єкти коректно знищуються
end note

@enduml

Практичні приклади


Конвертація між розумними покажчиками

std::unique_ptr → std::shared_ptr

std::unique_ptr можна перемістити в std::shared_ptr:

UniqueTo Shared.cpp
#include <iostream>
#include <memory>

class Widget
{
private:
    int m_id;

public:
    Widget(int id) : m_id(id)
    {
        std::cout << "Widget " << m_id << " created\n";
    }

    ~Widget()
    {
        std::cout << "Widget " << m_id << " destroyed\n";
    }

    int getId() const { return m_id; }
};

// Фабрика повертає unique_ptr
std::unique_ptr<Widget> createWidget(int id)
{
    return std::make_unique<Widget>(id);
}

int main()
{
    // Отримуємо unique_ptr
    auto uniqueWidget = createWidget(1);
    
    // ✅ Конвертуємо в shared_ptr через переміщення
    std::shared_ptr<Widget> sharedWidget = std::move(uniqueWidget);
    
    std::cout << "uniqueWidget is " << (uniqueWidget ? "not null" : "null") << "\n";
    std::cout << "sharedWidget refCount: " << sharedWidget.use_count() << "\n";
    
    // Тепер можемо створювати копії
    auto sharedWidget2 = sharedWidget;
    std::cout << "After copy: refCount = " << sharedWidget.use_count() << "\n";
    
    return 0;
}
./UniqueToShared
Widget 1 created
uniqueWidget is null
sharedWidget refCount: 1
After copy: refCount = 2
Widget 1 destroyed
Патерн повернення:
// ✅ Повертайте unique_ptr з фабрик
std::unique_ptr<Widget> createWidget();

// Виклик може обрати:
auto u = createWidget();              // Виключне володіння
std::shared_ptr<Widget> s = createWidget();  // Спільне володіння (auto-конвертація)

❌ std::shared_ptr → std::unique_ptr неможливо

SharedToUnique.cpp
std::shared_ptr<Widget> shared = std::make_shared<Widget>(1);

// ❌ Помилка компіляції:
// std::unique_ptr<Widget> unique = shared;
// std::unique_ptr<Widget> unique = std::move(shared);

// Чому? shared_ptr може мати кілька власників,
// unique_ptr вимагає ВИКЛЮЧНОГО володіння

std::enable_shared_from_this

Іноді об'єкт всередині свого методу повинен повернути shared_ptr на себе. Для цього використовується патерн std::enable_shared_from_this:

EnableSharedFromThis.cpp
#include <iostream>
#include <memory>
#include <vector>

class Node : public std::enable_shared_from_this<Node>
{
private:
    int m_value;

public:
    Node(int value) : m_value(value)
    {
        std::cout << "Node(" << m_value << ") created\n";
    }

    ~Node()
    {
        std::cout << "Node(" << m_value << ") destroyed\n";
    }

    // Метод, що повертає shared_ptr на себе
    std::shared_ptr<Node> getPtr()
    {
        return shared_from_this();
    }

    int getValue() const { return m_value; }
};

class NodeManager
{
private:
    std::vector<std::shared_ptr<Node>> m_nodes;

public:
    void addNode(std::shared_ptr<Node> node)
    {
        m_nodes.push_back(node);
        std::cout << "Node added to manager\n";
    }

    void printNodes() const
    {
        std::cout << "Nodes: ";
        for (const auto& node : m_nodes)
            std::cout << node->getValue() << " ";
        std::cout << "\n";
    }
};

int main()
{
    NodeManager manager;
    
    auto node = std::make_shared<Node>(42);
    std::cout << "Initial refCount: " << node.use_count() << "\n";
    
    // Об'єкт може зареєструвати себе в менеджері
    manager.addNode(node->getPtr());
    std::cout << "After registration: " << node.use_count() << "\n";
    
    manager.printNodes();
    
    return 0;
}
./EnableSharedFromThis
Node(42) created
Initial refCount: 1
Node added to manager
After registration: 2
Nodes: 42
Node(42) destroyed
Обмеження shared_from_this():
  1. Клас ПОВИНЕН успадковуватися від enable_shared_from_this<T>
  2. Об'єкт ПОВИНЕН керуватися shared_ptr (не stack, не unique_ptr)
  3. НЕ викликайте shared_from_this() в конструкторі!
class Widget : public std::enable_shared_from_this<Widget>
{
public:
    Widget()
    {
        // ❌ НЕБЕЗПЕЧНО — об'єкт ще не керується shared_ptr!
        // auto self = shared_from_this();
    }

    void init()
    {
        // ✅ Безпечно — викликається після створення shared_ptr
        auto self = shared_from_this();
    }
};

int main()
{
    auto w = std::make_shared<Widget>();
    w->init();  // ✅ Працює
}

Багатопоточність та std::shared_ptr

std::shared_ptr має обмежену підтримку багатопоточності:

✅ Потокобезпечні операції

  • Підрахунок посилань: атомарний (через std::atomic)
  • Копіювання/переміщення різних shared_ptr → безпечно
  • Одночасне читання з різних потоків → безпечно

❌ НЕ потокобезпечні операції

  • Зміна одного shared_ptr з кількох потоків → гонка даних
  • Зміна об'єкта, на який вказує shared_ptr → потребує синхронізації
  • reset() / operator= одного shared_ptr → не атомарні
ThreadSafety.cpp
#include <iostream>
#include <memory>
#include <thread>
#include <vector>

class Counter
{
private:
    int m_value = 0;

public:
    void increment()
    {
        ++m_value;  // ❌ Гонка даних!
    }

    int getValue() const { return m_value; }
};

int main()
{
    auto counter = std::make_shared<Counter>();
    
    std::vector<std::thread> threads;
    
    // ✅ Копіювання shared_ptr у кожен потік — безпечно
    for (int i = 0; i < 10; ++i)
    {
        threads.emplace_back([counter]() {  // Копія shared_ptr
            for (int j = 0; j < 1000; ++j)
                counter->increment();  // ❌ Але increment() не потокобезпечний!
        });
    }
    
    for (auto& t : threads)
        t.join();
    
    // Результат недетермінований через гонку даних
    std::cout << "Final value: " << counter->getValue() 
              << " (expected: 10000)\n";
    
    return 0;
}
Правило:std::shared_ptr гарантує потокобезпечність лічильника посилань, але не гарантує потокобезпечність самого об'єкта.Для синхронізації об'єкта використовуйте:
  • std::mutex / std::lock_guard
  • Атомарні операції (std::atomic<T>)
  • Незмінні об'єкти (const методи)

Продуктивність: накладні витрати

Порівняння розмірів

Тип покажчикаРозмір (64-біт)Heap алокації
Сирий вказівник T*8 байт0 (керується вручну)
std::unique_ptr<T>8 байт1 (об'єкт)
std::shared_ptr<T>16 байт2 (об'єкт + Control Block)
std::weak_ptr<T>16 байт0 (спостерігає Control Block)

Операції та їх вартість

`unique_ptr`

  • Створення: O(1) — виділення об'єкта
  • Копіювання: ❌ Заборонено
  • Переміщення: O(1) — переприсвоєння вказівника
  • Знищення: O(1) — виклик delete

`shared_ptr`

  • Створення: O(1) — об'єкт + Control Block
  • Копіювання: O(1) — атомарний increment refCount
  • Переміщення: O(1) — переприсвоєння вказівників
  • Знищення: O(1) — атомарний decrement refCount

`weak_ptr`

  • Створення: O(1) — копія вказівника на Control Block
  • Копіювання: O(1) — атомарний increment weakCount
  • lock(): O(1) — атомарна перевірка + створення shared_ptr
  • Знищення: O(1) — атомарний decrement weakCount
Рекомендації з продуктивності:
  1. Використовуйте unique_ptr за замовчуванням — найменші накладні витрати
  2. shared_ptr для спільного володіння — коли дійсно потрібне
  3. Уникайте зайвого копіювання shared_ptr — передавайте const shared_ptr& або T*
  4. make_shared швидше за shared_ptr(new T) — одна алокація замість двох

Вибір правильного розумного покажчика

Loading diagram...
@startuml
skinparam backgroundColor transparent
skinparam defaultFontName "JetBrains Mono"
skinparam defaultFontSize 12

start

if (Потрібна динамічна\nпам'ять?) then (ні)
    :Використовуйте звичайні змінні\n(stack allocation);
    stop
else (так)
    if (Один власник?) then (так)
        :**std::unique_ptr**;
        stop
    else (кілька власників)
        if (Треба розірвати\nциклічну залежність?) then (так)
            :**std::weak_ptr**\n(для одного напрямку);
            stop
        else (ні)
            :**std::shared_ptr**;
            stop
        endif
    endif
endif

@enduml

Таблиця вибору

СитуаціяРішенняОбґрунтування
Фабрична функціяunique_ptrCaller обирає семантику володіння
Контейнер поліморфних об'єктівunique_ptr або shared_ptrЗалежить від потреби копіювання
Кешweak_ptrНе утримує об'єкти примусово
Граф з цикламиshared_ptr + weak_ptrРозриває цикли
Дерево батько-дітиБатько: shared_ptr, Діти: weak_ptrУникає циклу
Спостерігач (Observer)weak_ptrПідписники можуть зникнути
PIMPL ідіомаunique_ptrВиключне володіння impl
Член класу (ексклюзивний)unique_ptrПростіше та швидше
Член класу (спільний)shared_ptrКоли потрібне спільне володіння

Поширені помилки

❌ Помилка 1: Створення shared_ptr з одного raw pointer двічі

DoubleSharedPtr.cpp
Widget* raw = new Widget(1);

std::shared_ptr<Widget> ptr1(raw);
std::shared_ptr<Widget> ptr2(raw);  // ❌ КАТАСТРОФА!

// ptr1 і ptr2 мають РІЗНІ Control Blocks
// → подвійне delete при знищенні!

Рішення: Завжди використовуйте make_shared або копіюйте існуючий shared_ptr.

❌ Помилка 2: Висячий weak_ptr без перевірки

DanglingWeakPtr.cpp
std::weak_ptr<Widget> weak;

{
    auto shared = std::make_shared<Widget>(1);
    weak = shared;
}  // shared знищується

// ❌ Помилка: не перевірили expired()
auto widget = weak.lock();
widget->doSomething();  // ← widget == nullptr!

Рішення: Завжди перевіряйте expired() або перевіряйте результат lock().

❌ Помилка 3: shared_from_this() на stack-об'єкті

SharedFromThisOnStack.cpp
class Widget : public std::enable_shared_from_this<Widget>
{
public:
    std::shared_ptr<Widget> getPtr()
    {
        return shared_from_this();
    }
};

int main()
{
    Widget widget;  // ❌ Stack-об'єкт
    auto ptr = widget.getPtr();  // ← std::bad_weak_ptr виняток!
}

Рішення: enable_shared_from_this працює ЛИШЕ з об'єктами, керованими shared_ptr.

❌ Помилка 4: Зайве копіювання shared_ptr

ExcessiveCopying.cpp
// ❌ Погано: копіює shared_ptr
void process(std::shared_ptr<Widget> widget)
{
    widget->doSomething();
}

// ✅ Краще: приймає посилання (якщо не зберігає)
void process(const std::shared_ptr<Widget>& widget)
{
    widget->doSomething();
}

// ✅ Найкраще: приймає raw pointer (якщо не володіє)
void process(Widget* widget)
{
    if (widget)
        widget->doSomething();
}

int main()
{
    auto widget = std::make_shared<Widget>();
    
    process(widget);         // Копіювання
    process(widget);         // Посилання (no-copy)
    process(widget.get());   // Raw pointer (no-copy)
}

Резюме

📌 `std::shared_ptr`

  • Спільне володіння: кілька власників одного ресурсу
  • Підрахунок посилань: автоматичне відстеження
  • Копіювання дозволене: збільшує refCount
  • Знищення: коли останній shared_ptr виходить з scope
  • Розмір: 16 байт (2 вказівники)

📌 `std::weak_ptr`

  • Спостерігач: не володіє ресурсом
  • Розриває цикли: вирішує циклічні залежності
  • Безпечний: автоматично стає nullptr
  • Використання: через lock() → shared_ptr
  • Розмір: 16 байт (як shared_ptr)

✅ Коли використовувати

shared_ptr:

  • Спільне володіння ресурсом
  • Контейнери з копіюванням
  • Графи об'єктів
  • Багатопоточність (розподілений доступ)

weak_ptr:

  • Кеші (не утримує примусово)
  • Розрив циклів у графах
  • Паттерн Observer
  • Зворотні посилання (батько ← дитина)

⚠️ Небезпеки

  • Циклічні залежності → витоки
  • Подвійний Control Block → crash
  • Зайве копіювання → overhead
  • Багатопоточність → потребує синхронізації об'єкта
Наступна тема: У статті 100 розглянемо контейнери STL — std::vector, std::list, std::map та алгоритми стандартної бібліотеки для ефективної роботи з колекціями.

Завдання для самоперевірки

  1. Що виведе програма?
    auto ptr1 = std::make_shared<int>(10);
    std::weak_ptr<int> weak = ptr1;
    ptr1.reset();
    
    if (auto ptr2 = weak.lock())
        std::cout << *ptr2;
    else
        std::cout << "nullptr";
    
  2. Чи є витік пам'яті?
    struct Node {
        std::shared_ptr<Node> next;
    };
    
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->next = n2;
    n2->next = n1;
    
  3. Як виправити?
    class Widget : public std::enable_shared_from_this<Widget> {
    public:
        Widget() {
            auto self = shared_from_this();  // ← Що не так?
        }
    };
    
  4. Коли обрати unique_ptr vs shared_ptr?
  1. Відповідь: Виведе nullptr. Після ptr1.reset() ресурс знищується (refCount = 0), тому weak.lock() повертає nullptr.
  2. Відповідь: Так, витік пам'яті! Циклічна залежність: n1 → n2 → n1. Обидва вузли мають refCount = 2, тому не знищуються. Рішення: замінити одне з полів next на std::weak_ptr<Node>.
  3. Відповідь: shared_from_this() НЕ можна викликати в конструкторі — об'єкт ще не керується shared_ptr. Рішення: створити метод init() і викликати його після створення shared_ptr.
  4. Відповідь:
    • unique_ptr: виключне володіння, один власник, легший overhead, не можна копіювати
    • shared_ptr: спільне володіння, кілька власників, більший overhead, можна копіювати

    Правило: використовуйте unique_ptr за замовчуванням, переходьте на shared_ptr лише якщо дійсно потрібне спільне володіння.
Copyright © 2026