Спільне володіння: std::shared_ptr та std::weak_ptr
Спільне володіння: std::shared_ptr та std::weak_ptr
Обмеження виключного володіння
У попередній статті ми розглянули std::unique_ptr — розумний покажчик з виключним володінням. Однак у реальних програмах часто виникають ситуації, коли кілька частин програми мають спільно володіти одним ресурсом:
#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;
}
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 - Копіювання дозволене: створення копії збільшує лічильник посилань
#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;
}
Фабрична функція std::make_shared
Як і з unique_ptr, для створення shared_ptr рекомендується використовувати std::make_shared<T>():
#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;
}
Переваги 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, але з додатковими методами для роботи з лічильником:
#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;
}
Основні методи std::shared_ptr
| Метод | Опис | Приклад |
|---|---|---|
use_count() | Повертає кількість власників | ptr.use_count() == 3 |
unique() | Чи є єдиним власником? | ptr.unique() == true |
get() | Повертає сирий вказівник | Data* raw = ptr.get(); |
reset() | Замінює ресурс або робить nullptr | ptr.reset(new Data(5)); |
operator* | Розіменування | (*ptr).setValue(10); |
operator-> | Доступ до членів | ptr->setValue(10); |
operator bool | Перевірка на nullptr | if (ptr) { ... } |
Блок управління (Control Block)
std::shared_ptr зберігає два вказівники всередині:
- Вказівник на об'єкт — керований ресурс
- Вказівник на блок управління — метадані для підрахунку посилань
Структура блоку управління
// Спрощена схема блоку управління (псевдокод)
struct ControlBlock
{
size_t refCount; // Кількість shared_ptr
size_t weakCount; // Кількість weak_ptr
void* resourcePtr; // Вказівник на ресурс
Deleter deleter; // Функція видалення
Allocator allocator; // Алокатор
};
sizeof(shared_ptr<T>) = 16 байт (на 64-бітних системах):- 8 байт — вказівник на об'єкт
- 8 байт — вказівник на блок управління
unique_ptr (8 байт), але все одно компактно.Копіювання та переміщення std::shared_ptr
На відміну від unique_ptr, std::shared_ptr підтримує копіювання:
#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;
}
Копіювання vs Переміщення
| Операція | Лічильник | ptr1 | ptr2 | ptr3 |
|---|---|---|---|---|
| Початок | 1 | ✅ (володіє) | — | — |
| Після copy | 2 ↑ | ✅ (володіє) | ✅ (володіє) | — |
| Після move | 2 (без змін) | ❌ (nullptr) | ✅ (володіє) | ✅ (володіє) |
- Збільшує
refCount - Обидва вказівники залишаються валідними
- Атомарна операція (безпечна для багатопоточності)
- НЕ змінює
refCount - Вихідний вказівник стає
nullptr - Швидше за копіювання (не потрібно змінювати лічильник)
Розв'язання проблеми спільного володіння
Тепер можемо переписати приклад з початку статті, використовуючи std::shared_ptr:
#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
Небезпека циклічних залежностей
std::shared_ptr має фундаментальну проблему — циклічні залежності (circular references), що призводять до витоків пам'яті:
#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;
}
alice і bob з scope:aliceвиходить →refCountAlice: 2 → 1 (залишивсяm_partnerBob) → не знищуєтьсяbobвиходить →refCountBob: 2 → 1 (залишивсяm_partnerAlice) → не знищується
std::weak_ptr: розв'язання циклічних залежностей
std::weak_ptr<T> — розумний покажчик-спостерігач (observer):
- НЕ володіє ресурсом (не збільшує
refCount) - Спостерігає за ресурсом, яким володіє
shared_ptr - Безпечний: автоматично стає
nullptr, якщо ресурс знищено - Конвертується в
shared_ptrчерез методlock()
#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;
}
Основні методи 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
Тепер виправимо приклад з циклічною залежністю:
#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;
}
Практичні приклади
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>
class Texture
{
private:
std::string m_filename;
public:
Texture(const std::string& filename) : m_filename(filename)
{
std::cout << "Texture '" << m_filename << "' loaded\n";
}
~Texture()
{
std::cout << "Texture '" << m_filename << "' unloaded\n";
}
const std::string& getFilename() const { return m_filename; }
};
class TextureCache
{
private:
// Зберігаємо weak_ptr — не утримуємо текстури примусово
std::unordered_map<std::string, std::weak_ptr<Texture>> m_cache;
public:
std::shared_ptr<Texture> loadTexture(const std::string& filename)
{
// Перевіряємо кеш
auto it = m_cache.find(filename);
if (it != m_cache.end())
{
// Спробуємо отримати shared_ptr з weak_ptr
if (auto texture = it->second.lock())
{
std::cout << "Cache hit: " << filename << "\n";
return texture;
}
else
{
// Текстура була знищена — видаляємо з кешу
std::cout << "Cache expired: " << filename << "\n";
m_cache.erase(it);
}
}
// Завантажуємо нову текстуру
std::cout << "Cache miss: loading " << filename << "\n";
auto texture = std::make_shared<Texture>(filename);
m_cache[filename] = texture; // Зберігаємо weak_ptr
return texture;
}
void printCacheStatus()
{
std::cout << "\n=== Cache Status ===\n";
for (const auto& [filename, weak] : m_cache)
{
if (weak.expired())
std::cout << " " << filename << ": EXPIRED\n";
else
std::cout << " " << filename << ": active (refCount="
<< weak.use_count() << ")\n";
}
std::cout << "====================\n\n";
}
};
int main()
{
TextureCache cache;
// Завантажуємо текстури
{
auto tex1 = cache.loadTexture("player.png");
auto tex2 = cache.loadTexture("enemy.png");
cache.printCacheStatus();
// Повторне завантаження — з кешу
auto tex3 = cache.loadTexture("player.png");
cache.printCacheStatus();
std::cout << "Exiting scope...\n";
} // Текстури знищуються (refCount → 0)
std::cout << "\nAfter scope:\n";
cache.printCacheStatus();
// Спроба завантажити знову — кеш протух
auto tex4 = cache.loadTexture("player.png");
return 0;
}
Результат:
Cache miss: loading player.png
Texture 'player.png' loaded
Cache miss: loading enemy.png
Texture 'enemy.png' loaded
=== Cache Status ===
player.png: active (refCount=1)
enemy.png: active (refCount=1)
====================
Cache hit: player.png
=== Cache Status ===
player.png: active (refCount=2)
enemy.png: active (refCount=1)
====================
Exiting scope...
Texture 'enemy.png' unloaded
Texture 'player.png' unloaded
After scope:
=== Cache Status ===
player.png: EXPIRED
enemy.png: EXPIRED
====================
Cache expired: player.png
Cache miss: loading player.png
Texture 'player.png' loaded
Texture 'player.png' unloaded
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class TreeNode
{
private:
std::string m_name;
std::weak_ptr<TreeNode> m_parent; // ✅ weak_ptr — уникаємо циклу
std::vector<std::shared_ptr<TreeNode>> m_children; // ✅ shared_ptr — володіємо дітьми
public:
TreeNode(const std::string& name) : m_name(name)
{
std::cout << "Node '" << m_name << "' created\n";
}
~TreeNode()
{
std::cout << "Node '" << m_name << "' destroyed\n";
}
void addChild(std::shared_ptr<TreeNode> child)
{
child->m_parent = shared_from_this(); // Встановлюємо weak_ptr на батька
m_children.push_back(child);
}
void printPath() const
{
std::cout << m_name;
if (auto parent = m_parent.lock())
{
std::cout << " → parent: ";
parent->printPath();
}
else
{
std::cout << " (root)";
}
std::cout << "\n";
}
void printTree(int depth = 0) const
{
for (int i = 0; i < depth; ++i)
std::cout << " ";
std::cout << "- " << m_name << "\n";
for (const auto& child : m_children)
child->printTree(depth + 1);
}
static std::shared_ptr<TreeNode> create(const std::string& name)
{
return std::make_shared<TreeNode>(name);
}
};
// Потрібно успадкуватися від enable_shared_from_this для shared_from_this()
class TreeNodeFixed : public std::enable_shared_from_this<TreeNodeFixed>
{
private:
std::string m_name;
std::weak_ptr<TreeNodeFixed> m_parent;
std::vector<std::shared_ptr<TreeNodeFixed>> m_children;
public:
TreeNodeFixed(const std::string& name) : m_name(name)
{
std::cout << "Node '" << m_name << "' created\n";
}
~TreeNodeFixed()
{
std::cout << "Node '" << m_name << "' destroyed\n";
}
void addChild(std::shared_ptr<TreeNodeFixed> child)
{
child->m_parent = shared_from_this();
m_children.push_back(child);
}
void printPath() const
{
std::cout << m_name;
if (auto parent = m_parent.lock())
{
std::cout << " → parent: ";
parent->printPath();
}
else
{
std::cout << " (root)";
}
std::cout << "\n";
}
void printTree(int depth = 0) const
{
for (int i = 0; i < depth; ++i)
std::cout << " ";
std::cout << "- " << m_name << "\n";
for (const auto& child : m_children)
child->printTree(depth + 1);
}
};
int main()
{
// Створюємо дерево
auto root = std::make_shared<TreeNodeFixed>("Root");
auto child1 = std::make_shared<TreeNodeFixed>("Child1");
auto child2 = std::make_shared<TreeNodeFixed>("Child2");
root->addChild(child1);
root->addChild(child2);
auto grandchild1 = std::make_shared<TreeNodeFixed>("Grandchild1");
auto grandchild2 = std::make_shared<TreeNodeFixed>("Grandchild2");
child1->addChild(grandchild1);
child1->addChild(grandchild2);
std::cout << "\n=== Tree Structure ===\n";
root->printTree();
std::cout << "\n=== Paths ===\n";
grandchild1->printPath();
grandchild2->printPath();
child2->printPath();
std::cout << "\nDestroying tree...\n";
return 0;
} // Каскадне знищення: root → children → grandchildren
Результат:
Node 'Root' created
Node 'Child1' created
Node 'Child2' created
Node 'Grandchild1' created
Node 'Grandchild2' created
=== Tree Structure ===
- Root
- Child1
- Grandchild1
- Grandchild2
- Child2
=== Paths ===
Grandchild1 → parent: Child1 → parent: Root (root)
Grandchild2 → parent: Child1 → parent: Root (root)
Child2 → parent: Root (root)
Destroying tree...
Node 'Root' destroyed
Node 'Child1' destroyed
Node 'Grandchild1' destroyed
Node 'Grandchild2' destroyed
Node 'Child2' destroyed
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <algorithm>
class Subscriber;
class Event
{
private:
std::string m_name;
public:
Event(const std::string& name) : m_name(name) {}
const std::string& getName() const { return m_name; }
};
class Subscriber
{
private:
std::string m_name;
public:
Subscriber(const std::string& name) : m_name(name)
{
std::cout << "Subscriber '" << m_name << "' created\n";
}
~Subscriber()
{
std::cout << "Subscriber '" << m_name << "' destroyed\n";
}
void onEvent(const Event& event)
{
std::cout << "[" << m_name << "] received event: "
<< event.getName() << "\n";
}
const std::string& getName() const { return m_name; }
};
class EventBus
{
private:
// weak_ptr дозволяє підписникам знищуватися незалежно
std::vector<std::weak_ptr<Subscriber>> m_subscribers;
public:
void subscribe(std::shared_ptr<Subscriber> subscriber)
{
m_subscribers.push_back(subscriber);
std::cout << "Subscribed: " << subscriber->getName() << "\n";
}
void publish(const Event& event)
{
std::cout << "\n=== Publishing event: " << event.getName() << " ===\n";
// Видаляємо протухлі підписки
m_subscribers.erase(
std::remove_if(m_subscribers.begin(), m_subscribers.end(),
[](const std::weak_ptr<Subscriber>& weak) {
return weak.expired();
}),
m_subscribers.end()
);
// Сповіщаємо активних підписників
for (const auto& weak : m_subscribers)
{
if (auto subscriber = weak.lock())
{
subscriber->onEvent(event);
}
}
std::cout << "Active subscribers: " << m_subscribers.size() << "\n";
}
};
int main()
{
EventBus bus;
auto sub1 = std::make_shared<Subscriber>("Sub1");
auto sub2 = std::make_shared<Subscriber>("Sub2");
auto sub3 = std::make_shared<Subscriber>("Sub3");
bus.subscribe(sub1);
bus.subscribe(sub2);
bus.subscribe(sub3);
bus.publish(Event("UserLogin"));
std::cout << "\nDestroying Sub2...\n";
sub2.reset(); // Видаляємо підписника
bus.publish(Event("DataUpdated"));
return 0;
}
Результат:
Subscriber 'Sub1' created
Subscriber 'Sub2' created
Subscriber 'Sub3' created
Subscribed: Sub1
Subscribed: Sub2
Subscribed: Sub3
=== Publishing event: UserLogin ===
[Sub1] received event: UserLogin
[Sub2] received event: UserLogin
[Sub3] received event: UserLogin
Active subscribers: 3
Destroying Sub2...
Subscriber 'Sub2' destroyed
=== Publishing event: DataUpdated ===
[Sub1] received event: DataUpdated
[Sub3] received event: DataUpdated
Active subscribers: 2
Subscriber 'Sub3' destroyed
Subscriber 'Sub1' destroyed
Конвертація між розумними покажчиками
std::unique_ptr → std::shared_ptr
std::unique_ptr можна перемістити в std::shared_ptr:
#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;
}
// ✅ Повертайте unique_ptr з фабрик
std::unique_ptr<Widget> createWidget();
// Виклик може обрати:
auto u = createWidget(); // Виключне володіння
std::shared_ptr<Widget> s = createWidget(); // Спільне володіння (auto-конвертація)
❌ std::shared_ptr → std::unique_ptr неможливо
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:
#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;
}
shared_from_this():- Клас ПОВИНЕН успадковуватися від
enable_shared_from_this<T> - Об'єкт ПОВИНЕН керуватися
shared_ptr(не stack, неunique_ptr) - НЕ викликайте
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→ не атомарні
#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
- Використовуйте
unique_ptrза замовчуванням — найменші накладні витрати shared_ptrдля спільного володіння — коли дійсно потрібне- Уникайте зайвого копіювання
shared_ptr— передавайтеconst shared_ptr&абоT* make_sharedшвидше заshared_ptr(new T)— одна алокація замість двох
Вибір правильного розумного покажчика
Таблиця вибору
| Ситуація | Рішення | Обґрунтування |
|---|---|---|
| Фабрична функція | unique_ptr | Caller обирає семантику володіння |
| Контейнер поліморфних об'єктів | 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 двічі
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 без перевірки
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-об'єкті
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
// ❌ Погано: копіює 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
- Багатопоточність → потребує синхронізації об'єкта
std::vector, std::list, std::map та алгоритми стандартної бібліотеки для ефективної роботи з колекціями.Завдання для самоперевірки
- Що виведе програма?
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"; - Чи є витік пам'яті?
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; - Як виправити?
class Widget : public std::enable_shared_from_this<Widget> { public: Widget() { auto self = shared_from_this(); // ← Що не так? } }; - Коли обрати
unique_ptrvsshared_ptr?
- Відповідь: Виведе
nullptr. Післяptr1.reset()ресурс знищується (refCount = 0), томуweak.lock()повертаєnullptr. - Відповідь: Так, витік пам'яті! Циклічна залежність:
n1→n2→n1. Обидва вузли маютьrefCount = 2, тому не знищуються. Рішення: замінити одне з полівnextнаstd::weak_ptr<Node>. - Відповідь:
shared_from_this()НЕ можна викликати в конструкторі — об'єкт ще не керуєтьсяshared_ptr. Рішення: створити методinit()і викликати його після створенняshared_ptr. - Відповідь:
unique_ptr: виключне володіння, один власник, легший overhead, не можна копіюватиshared_ptr: спільне володіння, кілька власників, більший overhead, можна копіювати
Правило: використовуйтеunique_ptrза замовчуванням, переходьте наshared_ptrлише якщо дійсно потрібне спільне володіння.