Розумний покажчик std::unique_ptr
Розумний покажчик std::unique_ptr
Проблема: витоки пам'яті та винятки
Розглянемо класичну проблему керування динамічною пам'яттю:
#include <iostream>
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void doWork() { std::cout << "Working...\n"; }
};
void processData(bool throwException)
{
Resource* resource = new Resource();
resource->doWork();
if (throwException)
throw std::runtime_error("Error!"); // ❌ resource не видалено!
delete resource; // ← Ніколи не досягається при винятку
}
int main()
{
try
{
processData(true);
}
catch (const std::exception& e)
{
std::cout << "Caught: " << e.what() << "\n";
}
std::cout << "Program continues...\n";
return 0;
}
Проблеми:
- При винятку
deleteне виконується → витік пам'яті - При ранньому
return— та сама проблема - Забути
delete— легко, особливо у складних функціях - Подвійне
delete— катастрофічна помилка
Концепція розумних покажчиків
Розумний покажчик (smart pointer) — це клас-обгортка над сирим вказівником, що використовує ідіому RAII для автоматичного керування ресурсами.
- Ресурс захоплюється в конструкторі
- Ресурс звільняється в деструкторі
- Гарантія: деструктор викликається завжди при виході з області видимості
Еволюція розумних покажчиків у C++
| Версія C++ | Розумний покажчик | Статус |
|---|---|---|
| C++98 | std::auto_ptr | ❌ Deprecated (видалено в C++17) |
| C++11 | std::unique_ptr | ✅ Основний інструмент |
| C++11 | std::shared_ptr | ✅ Для спільного володіння |
| C++11 | std::weak_ptr | ✅ Для розривання циклів |
std::unique_ptr — найбільш важливий і найчастіше використовується (~80% випадків).std::unique_ptr: семантика виключного володіння
std::unique_ptr<T> — розумний покажчик з семантикою виключного володіння (exclusive ownership):
- Рівно один
std::unique_ptrволодіє ресурсом у будь-який момент часу - Копіювання заборонене (copy constructor та copy assignment видалені)
- Переміщення дозволене (move constructor та move assignment реалізовані)
- Нульові накладні витрати (zero overhead):
sizeof(unique_ptr) == sizeof(void*)
#include <iostream>
#include <memory> // для std::unique_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void doWork() { std::cout << "Working...\n"; }
};
void processData(bool throwException)
{
std::unique_ptr<Resource> resource(new Resource());
resource->doWork();
if (throwException)
throw std::runtime_error("Error!");
// delete НЕ потрібен — resource автоматично знищиться!
}
int main()
{
try
{
processData(true);
}
catch (const std::exception& e)
{
std::cout << "Caught: " << e.what() << "\n";
}
std::cout << "Program continues...\n";
return 0;
}
✅ Переваги
- Автоматичне видалення при виході зі scope
- Безпека при винятках
- Неможливо забути
delete - Неможливо подвійне
delete
🎯 Семантика
- Один власник — завжди
- Копіювання =
delete - Переміщення = передача володіння
- Нульові накладні витрати
Фабрична функція std::make_unique
std::make_unique<T>() (C++14) — рекомендований спосіб створення std::unique_ptr:
#include <iostream>
#include <memory>
class Widget
{
private:
int m_id;
std::string m_name;
public:
Widget(int id, const std::string& name)
: m_id(id), m_name(name)
{
std::cout << "Widget " << m_id << " created\n";
}
~Widget()
{
std::cout << "Widget " << m_id << " destroyed\n";
}
void display() const
{
std::cout << "Widget #" << m_id << ": " << m_name << "\n";
}
};
int main()
{
// ❌ Старий спосіб (C++11)
std::unique_ptr<Widget> widget1(new Widget(1, "Old style"));
// ✅ Рекомендований спосіб (C++14+)
auto widget2 = std::make_unique<Widget>(2, "Modern style");
widget1->display();
widget2->display();
return 0;
}
Переваги std::make_unique
1️⃣ Безпека винятків
// ❌ НЕБЕЗПЕЧНО
processWidgets(
std::unique_ptr<Widget>(new Widget(1)),
mayThrow()
);
// Якщо mayThrow() викине виняток
// після new, але до створення unique_ptr
// → витік пам'яті!
// ✅ БЕЗПЕЧНО
processWidgets(
std::make_unique<Widget>(1),
mayThrow()
);
// make_unique атомарна —
// або обидві операції успішні, або жодна
2️⃣ Відсутність дублювання
// ❌ Дублювання типу
std::unique_ptr<VeryLongClassName> ptr(
new VeryLongClassName(args)
);
// ✅ Без дублювання
auto ptr = std::make_unique<
VeryLongClassName
>(args);
3️⃣ Читабельність
// ❌ Видно raw pointer
new Widget(1)
// ✅ Видно інтенцію
std::make_unique<Widget>(1)
// "Створюю унікальний ресурс"
4️⃣ Узгодженість
Однакова форма для всіх розумних покажчиків:
std::make_unique<T>()std::make_shared<T>()
std::make_unique<T>() замість new при створенні std::unique_ptr.Інтерфейс std::unique_ptr
Оператори доступу: * та ->
#include <iostream>
#include <memory>
struct Point
{
int x, y;
void print() const
{
std::cout << "(" << x << ", " << y << ")\n";
}
};
int main()
{
auto ptr = std::make_unique<Point>();
ptr->x = 10; // Оператор ->
ptr->y = 20;
ptr->print();
// Оператор * (розіменування)
Point& ref = *ptr;
ref.x = 30;
ref.y = 40;
ptr->print();
return 0;
}
Метод get(): отримання сирого вказівника
#include <iostream>
#include <memory>
void legacyFunction(int* rawPtr)
{
if (rawPtr)
std::cout << "Value: " << *rawPtr << "\n";
}
int main()
{
auto ptr = std::make_unique<int>(42);
// Передаємо в legacy-функцію, що потребує raw pointer
legacyFunction(ptr.get());
// ptr все ще володіє ресурсом
std::cout << "Still owned: " << *ptr << "\n";
return 0;
}
get() для подальшого використання:int* raw = ptr.get();
ptr.reset(); // ресурс видалено
*raw; // ❌ Невизначена поведінка!
Метод reset(): заміна ресурсу
auto ptr = std::make_unique<int>(10);
std::cout << *ptr << "\n"; // 10
ptr.reset(new int(20)); // Видаляє 10, зберігає 20
std::cout << *ptr << "\n"; // 20
ptr.reset(); // Видаляє 20, ptr стає nullptr
Метод release(): передача володіння
auto ptr = std::make_unique<int>(100);
int* raw = ptr.release(); // ptr більше НЕ володіє ресурсом
// Тепер МИ відповідаємо за видалення!
std::cout << *raw << "\n";
delete raw; // Обов'язково!
release()не видаляє ресурс, а лише передає відповідальність. Використовуйте рідко — зазвичай для взаємодії з C-API.Перевірка на nullptr
std::unique_ptr<int> ptr; // nullptr за замовчуванням
if (ptr) // Неявна конвертація в bool
std::cout << *ptr;
else
std::cout << "ptr is null\n";
ptr = std::make_unique<int>(42);
if (ptr)
std::cout << "Value: " << *ptr << "\n";
Заборона копіювання, підтримка переміщення
std::unique_ptr реалізує Правило п'яти з видаленим копіюванням:
#include <iostream>
#include <memory>
#include <utility>
int main()
{
auto ptr1 = std::make_unique<int>(100);
// ❌ Копіювання заборонене
// auto ptr2 = ptr1; // Помилка компіляції
// ✅ Переміщення дозволене
auto ptr2 = std::move(ptr1);
std::cout << "ptr1 is " << (ptr1 ? "not null" : "null") << "\n";
std::cout << "ptr2 is " << (ptr2 ? "not null" : "null") << "\n";
std::cout << "Value: " << *ptr2 << "\n";
return 0;
}
Стан після std::move
| Name | Owns Resource | Value |
|---|---|---|
| ptr1 | ❌ (nullptr) | — |
| ptr2 | ✅ | 100 |
Повернення std::unique_ptr з функції
Розумний вказівник std::unique_ptr можна безпечно повертати з функції по значенню:
#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; }
};
// Фабрична функція: повертає unique_ptr по значенню
std::unique_ptr<Resource> createResource(int id)
{
return std::make_unique<Resource>(id);
}
int main()
{
// Семантика переміщення автоматично застосовується
auto resource = createResource(42);
std::cout << "Using resource " << resource->getId() << "\n";
return 0;
}
std::unique_ptr по значенню компілятор автоматично використовує семантику переміщення (не потрібен std::move()):return std::make_unique<T>(); // ✅ Автоматичне переміщення
return std::move(ptr); // ❌ Зайве — компілятор оптимізує
Фабричний патерн з std::unique_ptr
#include <iostream>
#include <memory>
#include <string>
class Shape
{
public:
virtual ~Shape() = default;
virtual void draw() const = 0;
};
class Circle : public Shape
{
public:
void draw() const override
{
std::cout << "Drawing Circle\n";
}
};
class Rectangle : public Shape
{
public:
void draw() const override
{
std::cout << "Drawing Rectangle\n";
}
};
// Фабрична функція: створює різні типи Shape
std::unique_ptr<Shape> createShape(const std::string& type)
{
if (type == "circle")
return std::make_unique<Circle>();
else if (type == "rectangle")
return std::make_unique<Rectangle>();
else
return nullptr; // Невідомий тип
}
int main()
{
auto shape1 = createShape("circle");
if (shape1)
shape1->draw();
auto shape2 = createShape("rectangle");
if (shape2)
shape2->draw();
auto shape3 = createShape("triangle");
if (!shape3)
std::cout << "Unknown shape type\n";
return 0;
}
✅ Переваги фабрик
- Безпечне повернення: ресурс не може "загубитися"
- Інкапсуляція: логіка створення відокремлена
- Поліморфізм: повертаємо базовий клас
- Чистота: не потрібен
delete
🎯 Патерни
- Фабричний метод
- Абстрактна фабрика
- Будівельник (Builder)
- Прототип
Передача std::unique_ptr у функцію
Існує чотири способи передачі std::unique_ptr у функцію, кожен з різною семантикою:
1️⃣ Передача по значенню: передача володіння
#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; }
};
// Функція ПРИЙМАЄ володіння — Widget знищиться в кінці функції
void takeOwnership(std::unique_ptr<Widget> widget)
{
std::cout << "Function owns Widget " << widget->getId() << "\n";
} // ← Widget знищується ТУТ
int main()
{
auto widget = std::make_unique<Widget>(1);
// ПОТРІБЕН std::move() — копіювання заборонене
takeOwnership(std::move(widget));
std::cout << "widget is " << (widget ? "not null" : "null") << "\n";
std::cout << "End of main\n";
return 0;
}
std::move(widget) змінна widget стає nullptr. Використовувати її надалі небезпечно!2️⃣ Передача по посиланню: тимчасове використання з можливістю змінити
// Функція може ЗМІНИТИ unique_ptr (наприклад, reset())
void useAndModify(std::unique_ptr<Widget>& widget)
{
if (widget)
std::cout << "Using Widget " << widget->getId() << "\n";
// Можна змінити сам покажчик
widget.reset(); // Видаляємо Widget, встановлюємо nullptr
std::cout << "Widget was destroyed\n";
}
int main()
{
auto widget = std::make_unique<Widget>(2);
useAndModify(widget); // Передаємо по посиланню
std::cout << "widget is " << (widget ? "not null" : "null") << "\n";
return 0;
}
3️⃣ Передача сирого вказівника через get(): тільки читання/використання
// Функція ВИКОРИСТОВУЄ Widget, але НЕ змінює володіння
void useWidget(const Widget* widget)
{
if (widget)
std::cout << "Using Widget " << widget->getId() << "\n";
}
int main()
{
auto widget = std::make_unique<Widget>(3);
// Передаємо raw pointer — володіння залишається в main()
useWidget(widget.get());
std::cout << "widget is still " << (widget ? "not null" : "null") << "\n";
return 0;
} // ← Widget знищується ТУТ
4️⃣ Передача по посиланню на об'єкт: використання без overhead
// Функція працює з САМИМ об'єктом (не з unique_ptr)
void processWidget(Widget& widget)
{
std::cout << "Processing Widget " << widget.getId() << "\n";
}
int main()
{
auto widget = std::make_unique<Widget>(4);
// Розіменовуємо unique_ptr і передаємо посилання
processWidget(*widget);
return 0;
}
- По значенню (
std::unique_ptr<T>) → якщо функція має ВОЛОДІТИ ресурсом - По сирому вказівнику (
T*) → якщо функція ВИКОРИСТОВУЄ ресурс (може бутиnullptr) - По посиланню (
T&) → якщо функція ВИКОРИСТОВУЄ ресурс (не може бутиnullptr) - По посиланню на
unique_ptr(std::unique_ptr<T>&) → якщо функція ЗМІНЮЄ сам покажчик
Таблиця вибору способу передачі
| Інтенція | Спосіб передачі | Володіння | nullptr можливий? |
|---|---|---|---|
| Функція стає власником | std::unique_ptr<T> (move) | Передається | — |
| Функція змінює покажчик | std::unique_ptr<T>& | Залишається в caller | ✅ |
| Функція використовує об'єкт | T* (через get()) | Залишається в caller | ✅ |
| Функція використовує об'єкт | T& (через *ptr) | Залишається в caller | ❌ |
std::unique_ptr і масиви: std::unique_ptr<T[]>
std::unique_ptr має спеціалізацію для масивів — std::unique_ptr<T[]>:
#include <iostream>
#include <memory>
int main()
{
// ❌ Старий спосіб (C-масив)
int* arr1 = new int[5];
// ... використання ...
delete[] arr1; // Легко забути!
// ✅ Розумний вказівник для масиву
auto arr2 = std::make_unique<int[]>(5);
// Ініціалізація
for (int i = 0; i < 5; ++i)
arr2[i] = i * 10;
// Доступ через оператор []
for (int i = 0; i < 5; ++i)
std::cout << arr2[i] << " ";
std::cout << "\n";
return 0;
} // delete[] викликається автоматично!
std::unique_ptr<T[]> використовує delete[], а не delete:std::unique_ptr<int[]> arr(new int[10]); // ✅ Правильно
// ~unique_ptr() викличе delete[]
std::unique_ptr<int> ptr(new int); // delete
std::unique_ptr<int[]> arr(new int[10]); // delete[]
Порівняння: std::unique_ptr<T[]> vs std::vector
#include <iostream>
#include <memory>
#include <vector>
int main()
{
// std::unique_ptr<int[]>
auto arr = std::make_unique<int[]>(5);
// ❌ Не знаємо розмір!
// ❌ Не можна змінити розмір
// ❌ Немає методів (size(), push_back())
// std::vector
std::vector<int> vec(5);
// ✅ Знаємо розмір: vec.size()
// ✅ Можна змінити: vec.resize(), vec.push_back()
// ✅ Багато методів STL
return 0;
}
std::vector<T> замість std::unique_ptr<T[]>:// ❌ Старий стиль
auto arr = std::make_unique<int[]>(100);
// ✅ Сучасний стиль
std::vector<int> vec(100);
Кастомні делетери
std::unique_ptr підтримує кастомні делетери (custom deleters) для ресурсів, що потребують спеціальної логіки звільнення:
Базовий приклад: функція-делетер
#include <iostream>
#include <memory>
class File
{
private:
std::string m_filename;
public:
File(const std::string& filename) : m_filename(filename)
{
std::cout << "Opening file: " << m_filename << "\n";
}
~File()
{
std::cout << "Closing file: " << m_filename << "\n";
}
void write(const std::string& data)
{
std::cout << "Writing to " << m_filename << ": " << data << "\n";
}
};
// Кастомний делетер
void customFileDeleter(File* file)
{
std::cout << "Custom deleter called!\n";
delete file;
}
int main()
{
// Синтаксис: std::unique_ptr<T, DeleterType>
std::unique_ptr<File, decltype(&customFileDeleter)> file(
new File("data.txt"),
customFileDeleter
);
file->write("Hello, World!");
return 0;
}
Лямбда як делетер
#include <iostream>
#include <memory>
int main()
{
// Лямбда-делетер
auto deleter = [](int* ptr) {
std::cout << "Deleting int with value: " << *ptr << "\n";
delete ptr;
};
std::unique_ptr<int, decltype(deleter)> ptr(
new int(42),
deleter
);
std::cout << "Value: " << *ptr << "\n";
return 0;
}
Практичний приклад: RAII для C-API
#include <iostream>
#include <memory>
#include <cstdio> // для FILE*
// Делетер для FILE*
struct FileDeleter
{
void operator()(FILE* file) const
{
if (file)
{
std::cout << "Closing file with fclose()\n";
fclose(file);
}
}
};
// Алісе для зручності
using FilePtr = std::unique_ptr<FILE, FileDeleter>;
int main()
{
// RAII для FILE* (C-API)
FilePtr file(fopen("test.txt", "w"));
if (!file)
{
std::cerr << "Failed to open file\n";
return 1;
}
fprintf(file.get(), "Hello from C API!\n");
return 0;
} // fclose() викликається автоматично через FileDeleter
🎯 Застосування
- C-API:
FILE*,sqlite3*, сокети - Системні ресурси: файлові дескриптори, м'ютекси
- Зовнішні бібліотеки: OpenGL текстури, CUDA пам'ять
- Логування: додавання логів при видаленні
⚠️ Обмеження
// Тип делетера стає ЧАСТИНОЮ типу
std::unique_ptr<int, MyDeleter1> p1;
std::unique_ptr<int, MyDeleter2> p2;
// p1 і p2 — РІЗНІ типи!
// Це НЕ скомпілюється:
// p1 = std::move(p2); // ❌ Різні типи
std::unique_ptr і поліморфізм
std::unique_ptr чудово працює з поліморфізмом через вказівники на базовий клас:
#include <iostream>
#include <memory>
#include <vector>
class Animal
{
public:
virtual ~Animal() = default; // ← КРИТИЧНО важливо!
virtual void makeSound() const = 0;
};
class Dog : public Animal
{
public:
void makeSound() const override
{
std::cout << "Woof!\n";
}
~Dog()
{
std::cout << "Dog destroyed\n";
}
};
class Cat : public Animal
{
public:
void makeSound() const override
{
std::cout << "Meow!\n";
}
~Cat()
{
std::cout << "Cat destroyed\n";
}
};
int main()
{
// Вектор розумних вказівників на базовий клас
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<Dog>());
// Поліморфні виклики
for (const auto& animal : animals)
animal->makeSound();
return 0;
} // Всі тварини коректно знищуються (викликаються віртуальні деструктори)
class Base
{
public:
virtual ~Base() = default; // ✅ Віртуальний деструктор
virtual void foo() = 0;
};
// ❌ БЕЗ віртуального деструктора:
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// ~ptr викличе ~Base(), а НЕ ~Derived() → витік ресурсів!
// ✅ З віртуальним деструктором:
// ~ptr правильно викличе ~Derived(), потім ~Base()
Приклад: система команд (Command pattern)
#include <iostream>
#include <memory>
#include <vector>
// Абстрактна команда
class Command
{
public:
virtual ~Command() = default;
virtual void execute() const = 0;
};
// Конкретні команди
class PrintCommand : public Command
{
private:
std::string m_message;
public:
PrintCommand(const std::string& message) : m_message(message) {}
void execute() const override
{
std::cout << "Print: " << m_message << "\n";
}
};
class CalculateCommand : public Command
{
private:
int m_a, m_b;
public:
CalculateCommand(int a, int b) : m_a(a), m_b(b) {}
void execute() const override
{
std::cout << "Calculate: " << m_a << " + " << m_b
<< " = " << (m_a + m_b) << "\n";
}
};
// Інвокер
class CommandExecutor
{
private:
std::vector<std::unique_ptr<Command>> m_commands;
public:
void addCommand(std::unique_ptr<Command> command)
{
m_commands.push_back(std::move(command));
}
void executeAll()
{
for (const auto& cmd : m_commands)
cmd->execute();
}
};
int main()
{
CommandExecutor executor;
executor.addCommand(std::make_unique<PrintCommand>("Hello"));
executor.addCommand(std::make_unique<CalculateCommand>(5, 7));
executor.addCommand(std::make_unique<PrintCommand>("World"));
executor.executeAll();
return 0;
}
std::unique_ptr як член класу
std::unique_ptr часто використовується як член класу для реалізації ідіоми PIMPL (Pointer to Implementation):
// Widget.h
#include <memory>
class Widget
{
private:
// Forward declaration
class WidgetImpl;
// Розумний вказівник на реалізацію
std::unique_ptr<WidgetImpl> m_impl;
public:
Widget();
~Widget(); // ← Потрібен в .cpp (де WidgetImpl визначений)
// Заборона копіювання (unique_ptr не копіюється)
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
// Переміщення дозволене
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
void doSomething();
};
// Widget.cpp
#include "Widget.h"
#include <iostream>
// Повне визначення реалізації (приховано від клієнтів)
class Widget::WidgetImpl
{
public:
void doSomethingInternal()
{
std::cout << "Widget implementation working...\n";
}
};
Widget::Widget()
: m_impl(std::make_unique<WidgetImpl>())
{
}
// Деструктор ПОВИНЕН бути в .cpp (де WidgetImpl визначений)
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::doSomething()
{
m_impl->doSomethingInternal();
}
✅ Переваги PIMPL
- Приховування реалізації: клієнти не бачать приватних членів
- Швидка компіляція: зміни реалізації не перекомпілюють клієнтів
- ABI стабільність: розмір класу не змінюється
- Відсутність залежностей: заголовки реалізації приховані
⚠️ Важливо
class Widget
{
std::unique_ptr<Impl> m_impl;
public:
// ❌ Деструктор в .h:
~Widget() = default;
// Помилка: sizeof(Impl) невідомий!
// ✅ Деструктор в .cpp:
~Widget(); // оголошення
};
// Widget.cpp
Widget::~Widget() = default; // визначення
Поширені помилки з std::unique_ptr
❌ Помилка 1: Подвійне володіння
Widget* rawWidget = new Widget();
std::unique_ptr<Widget> ptr1(rawWidget);
std::unique_ptr<Widget> ptr2(rawWidget); // ❌ НЕБЕЗПЕЧНО!
// При виході з scope обидва ptr1 і ptr2 спробують delete rawWidget
// → подвійне видалення → невизначена поведінка!
unique_ptr володіють одним ресурсом.Рішення: Завжди використовуйте std::make_unique():auto ptr = std::make_unique<Widget>(); // ✅ Безпечно
❌ Помилка 2: Ручне видалення під unique_ptr
Widget* rawWidget = new Widget();
std::unique_ptr<Widget> ptr(rawWidget);
delete rawWidget; // ❌ Видаляємо вручну
// ptr все ще вважає, що володіє ресурсом
// При виході з scope спробує delete — невизначена поведінка!
❌ Помилка 3: Зберігання результату get()
auto ptr = std::make_unique<int>(42);
int* raw = ptr.get(); // Отримали сирий вказівник
ptr.reset(); // Видалили ресурс
std::cout << *raw; // ❌ Невизначена поведінка — вказівник "мертвий"!
get() повертає неволодіючий вказівник. НЕ зберігайте його для подальшого використання після того, як unique_ptr може змінитися або знищитися.❌ Помилка 4: Відсутній віртуальний деструктор
class Base
{
public:
// ❌ Немає virtual ~Base()
};
class Derived : public Base
{
private:
int* m_data;
public:
Derived() : m_data(new int[100]) {}
~Derived() { delete[] m_data; }
};
std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// При знищенні ptr:
// викликається ~Base(), а НЕ ~Derived()
// → m_data не видаляється → витік пам'яті!
Рішення:
class Base
{
public:
virtual ~Base() = default; // ✅ Віртуальний деструктор
};
❌ Помилка 5: Копіювання замість переміщення
void takeOwnership(std::unique_ptr<Widget> widget)
{
// Функція володіє widget
}
auto ptr = std::make_unique<Widget>();
takeOwnership(ptr); // ❌ Помилка компіляції — копіювання заборонене!
// ✅ Правильно:
takeOwnership(std::move(ptr));
Практичні приклади
#include <iostream>
#include <memory>
#include <unordered_map>
#include <string>
class Texture
{
private:
std::string m_name;
public:
Texture(const std::string& name) : m_name(name)
{
std::cout << "Texture loaded: " << m_name << "\n";
}
~Texture()
{
std::cout << "Texture unloaded: " << m_name << "\n";
}
const std::string& getName() const { return m_name; }
};
class ResourceManager
{
private:
std::unordered_map<std::string, std::unique_ptr<Texture>> m_textures;
public:
// Завантаження текстури (повертаємо неволодіючий вказівник)
Texture* loadTexture(const std::string& name)
{
// Перевіряємо, чи вже завантажена
auto it = m_textures.find(name);
if (it != m_textures.end())
{
std::cout << "Texture already loaded: " << name << "\n";
return it->second.get();
}
// Завантажуємо нову
auto texture = std::make_unique<Texture>(name);
Texture* rawPtr = texture.get();
m_textures[name] = std::move(texture);
return rawPtr;
}
// Вивантаження текстури
void unloadTexture(const std::string& name)
{
m_textures.erase(name);
}
// Вивантаження всіх
void unloadAll()
{
m_textures.clear();
}
};
int main()
{
ResourceManager manager;
Texture* tex1 = manager.loadTexture("player.png");
Texture* tex2 = manager.loadTexture("enemy.png");
Texture* tex3 = manager.loadTexture("player.png"); // Вже завантажена
std::cout << "\nUsing textures...\n\n";
manager.unloadTexture("enemy.png");
std::cout << "\nExiting...\n";
return 0;
} // Всі залишені текстури автоматично вивантажуються
Результат виконання:
Texture loaded: player.png
Texture loaded: enemy.png
Texture already loaded: player.png
Using textures...
Texture unloaded: enemy.png
Exiting...
Texture unloaded: player.png
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class SceneNode
{
private:
std::string m_name;
std::vector<std::unique_ptr<SceneNode>> m_children;
SceneNode* m_parent; // Неволодіючий вказівник
public:
SceneNode(const std::string& name)
: m_name(name), m_parent(nullptr)
{
std::cout << "SceneNode created: " << m_name << "\n";
}
~SceneNode()
{
std::cout << "SceneNode destroyed: " << m_name << "\n";
}
void addChild(std::unique_ptr<SceneNode> child)
{
child->m_parent = this;
m_children.push_back(std::move(child));
}
void print(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->print(depth + 1);
}
const std::string& getName() const { return m_name; }
};
int main()
{
auto root = std::make_unique<SceneNode>("Root");
auto child1 = std::make_unique<SceneNode>("Child1");
child1->addChild(std::make_unique<SceneNode>("Grandchild1"));
child1->addChild(std::make_unique<SceneNode>("Grandchild2"));
root->addChild(std::move(child1));
root->addChild(std::make_unique<SceneNode>("Child2"));
std::cout << "\nScene hierarchy:\n";
root->print();
std::cout << "\nDestroying scene...\n";
return 0;
} // Каскадне знищення: root → children → grandchildren
Результат виконання:
SceneNode created: Root
SceneNode created: Child1
SceneNode created: Grandchild1
SceneNode created: Grandchild2
SceneNode created: Child2
Scene hierarchy:
- Root
- Child1
- Grandchild1
- Grandchild2
- Child2
Destroying scene...
SceneNode destroyed: Root
SceneNode destroyed: Child1
SceneNode destroyed: Grandchild1
SceneNode destroyed: Grandchild2
SceneNode destroyed: Child2
#include <iostream>
#include <memory>
#include <unordered_map>
#include <functional>
#include <string>
// Базовий клас продукту
class Product
{
public:
virtual ~Product() = default;
virtual void use() const = 0;
};
// Конкретні продукти
class ProductA : public Product
{
public:
void use() const override
{
std::cout << "Using Product A\n";
}
};
class ProductB : public Product
{
public:
void use() const override
{
std::cout << "Using Product B\n";
}
};
// Фабрика з динамічною реєстрацією
class ProductFactory
{
private:
using Creator = std::function<std::unique_ptr<Product>()>;
std::unordered_map<std::string, Creator> m_creators;
public:
// Реєстрація типу продукту
void registerProduct(const std::string& name, Creator creator)
{
m_creators[name] = creator;
}
// Створення продукту
std::unique_ptr<Product> createProduct(const std::string& name) const
{
auto it = m_creators.find(name);
if (it != m_creators.end())
return it->second();
std::cerr << "Unknown product: " << name << "\n";
return nullptr;
}
};
int main()
{
ProductFactory factory;
// Реєстрація типів
factory.registerProduct("A", []() { return std::make_unique<ProductA>(); });
factory.registerProduct("B", []() { return std::make_unique<ProductB>(); });
// Створення продуктів
auto product1 = factory.createProduct("A");
if (product1)
product1->use();
auto product2 = factory.createProduct("B");
if (product2)
product2->use();
auto product3 = factory.createProduct("C");
return 0;
}
Результат виконання:
Using Product A
Using Product B
Unknown product: C
Резюме
🎯 Ключові принципи
- Виключне володіння: рівно один власник
- Move-only: копіювання заборонене
- RAII: автоматичне видалення при виході з scope
- Zero overhead: розмір як у сирого вказівника
✅ Коли використовувати
- Динамічне виділення об'єктів
- Фабричні функції (повернення по значенню)
- Члени класу (PIMPL ідіома)
- Контейнери поліморфних об'єктів
📋 Правила
- Завжди
std::make_unique<T>() - Віртуальний деструктор для базових класів
get()тільки для тимчасового доступу- Передача: по значенню = передача володіння
🔍 Альтернативи
std::shared_ptr→ спільне володінняstd::weak_ptr→ спостереження без володінняstd::vector<T>→ замістьunique_ptr<T[]>- Звичайні значення → якщо не потрібна динаміка
std::shared_ptr — розумний вказівник з семантикою спільного володіння (shared ownership) та підрахунком посилань (reference counting).Завдання для самоперевірки
- Що станеться?
auto ptr = std::make_unique<int>(10); auto ptr2 = ptr; - Як виправити?
void process(std::unique_ptr<Widget> widget); auto w = std::make_unique<Widget>(); process(w); // ← Помилка компіляції - Чи коректний код?
std::unique_ptr<Base> createDerived() { return std::make_unique<Derived>(); } - Який делетер викличеться?
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
- Помилка компіляції:
unique_ptrне може копіюватися. Потрібенstd::move(ptr). - Виправлення:
process(std::move(w));— передача володіння через переміщення. - Так, коректний: Повернення по значенню автоматично використовує move-семантику. Коваріантність типів дозволяє повертати
Derived*якBase*. delete[]: Спеціалізаціяunique_ptr<T[]>автоматично використовуєdelete[]замістьdelete.
Move-конструктор, move-присвоювання та Правило п
Детальна реалізація конструктора переміщення та оператора присвоювання переміщенням, критична важливість noexcept, Rule of Five та Rule of Zero для сучасного C++.
Спільне володіння: std::shared_ptr та std::weak_ptr
Підрахунок посилань, циклічні залежності, блок управління та слабкі вказівники. Розумні покажчики для складних графів об\