Управление памятью на C++ не должно быть источником стресса. С тех пор как в C++11 появился unique_ptr, изменилась работа с динамической памятью, так что при минимальных накладных расходах на производительность утечки памяти стали практически невозможны.
Посмотрим, как это осуществляется на практике.
Чем отличается unique_ptr?
unique_ptr владеет тем, на что указывает. Когда же unique_ptr уничтожается, управляемый им объект автоматически удаляется. Это означает:
- Отсутствие ручного управления памятью.
- Отсутствие утечек памяти.
- Четкая семантика владения.
- Нулевые затраты времени выполнения по сравнению с необработанными указателями.
Вот простой пример:
void oldWay() {
MyClass* ptr = new MyClass();
// ... что-то делается ...
delete ptr; // Но это легко забыть
}
void betterWay() {
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
// ... что-то делается ...
// Удалять не нужно, очистка выполняется автоматически
}
Создание объектов unique_ptr
Создавать объекты unique_ptr всегда безопаснее и эффективнее при помощи std::make_unique:
// Рекомендуемый способ
auto ptr = std::make_unique<MyClass>(arg1, arg2);
// Не делайте этого
std::unique_ptr<MyClass> ptr(new MyClass(arg1, arg2));
Почему make_unique?
- Им предотвращаются утечки памяти в аргументах функции.
- Им гарантируется безопасность исключений.
- Он лаконичнее и удобнее для восприятия.
Работа с unique_ptr на практике
Доступ к управляемому объекту
class Device {
public:
void start() { /* ... */ }
void stop() { /* ... */ }
};
auto device = std::make_unique<Device>();
// Использование оператора-стрелочки
device->start();
// Использование оператора разыменования
(*device).stop();
// Получение необработанного указателя, но будьте осторожны
Device* rawPtr = device.get();
Перемещение владения
unique_ptr не копируется, зато перемещается:
std::unique_ptr<Device> createDevice() {
return std::make_unique<Device>();
}
void useDevice() {
// Владение перемещается из «createDevice» в «myDevice»
auto myDevice = createDevice();
// Владение перемещается к другому «unique_ptr»
std::unique_ptr<Device> otherDevice = std::move(myDevice);
// «myDevice» теперь нулевой
assert(myDevice == nullptr);
}
Пользовательские функции-удалители
Иногда нужна специальная логика очистки. Что происходит при уничтожении объекта, определяется пользовательскими функциями-удалителями:
struct FileDeleter {
void operator()(FILE* file) const {
if (file) {
fclose(file);
std::cout << "File closed\n";
}
}
};
void processFile(const char* filename) {
std::unique_ptr<FILE, FileDeleter> file(fopen(filename, "r"));
if (!file) {
throw std::runtime_error("Could not open file");
}
// Когда функция завершится, файл закроется автоматически
}
Реальные применения
1. Управление ресурсами в классах
class AudioPlayer {
private:
std::unique_ptr<AudioBuffer> buffer;
std::unique_ptr<Decoder> decoder;
public:
AudioPlayer()
: buffer(std::make_unique<AudioBuffer>())
, decoder(std::make_unique<Decoder>())
{}
// Деструктор не нужен — очистка автоматическая
// Конструктор перемещения
AudioPlayer(AudioPlayer&& other) = default;
// Присваивание перемещением
AudioPlayer& operator=(AudioPlayer&& other) = default;
// Операции копирования удаляются по умолчанию
AudioPlayer(const AudioPlayer&) = delete;
AudioPlayer& operator=(const AudioPlayer&) = delete;
};
2. Фабричные функции
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() override { /* ... */ }
};
class Square : public Shape {
public:
void draw() override { /* ... */ }
};
std::unique_ptr<Shape> createShape(const std::string& type) {
if (type == "circle") {
return std::make_unique<Circle>();
} else if (type == "square") {
return std::make_unique<Square>();
}
throw std::invalid_argument("Unknown shape type");
}
3. Реализация кэширования
class Cache {
private:
struct CacheEntry {
std::string data;
time_t timestamp;
};
std::unordered_map<std::string, std::unique_ptr<CacheEntry>> entries;
public:
void insert(const std::string& key, std::string data) {
auto entry = std::make_unique<CacheEntry>();
entry->data = std::move(data);
entry->timestamp = time(nullptr);
entries[key] = std::move(entry);
}
std::string get(const std::string& key) {
auto it = entries.find(key);
if (it != entries.end()) {
return it->second->data;
}
throw std::out_of_range("Key not found");
}
};
4. Потокобезопасная очередь
template<typename T>
class ThreadSafeQueue {
private:
struct Node {
std::unique_ptr<Node> next;
T data;
};
std::unique_ptr<Node> head;
Node* tail;
std::mutex mutex;
public:
void push(T value) {
auto new_node = std::make_unique<Node>();
new_node->data = std::move(value);
std::lock_guard<std::mutex> lock(mutex);
if (tail) {
tail->next = std::move(new_node);
tail = tail->next.get();
} else {
head = std::move(new_node);
tail = head.get();
}
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mutex);
if (!head) {
return false;
}
value = std::move(head->data);
head = std::move(head->next);
if (!head) {
tail = nullptr;
}
return true;
}
};
Типичные проблемы и их решения
1. Циклические ссылки
class Parent;
class Child;
class Parent {
std::unique_ptr<Child> child; // Без проблем
};
class Child {
std::unique_ptr<Parent> parent; // Проблема: создается циклическая зависимость
Parent* parent; // Решение: использовать необработанный указатель или «weak_ptr»
};
2. Массивы с unique_ptr
// Не делайте этого
std::unique_ptr<int> arrayPtr(new int[10]); // Неправильная функция-удалитель
// Делайте так
std::unique_ptr<int[]> arrayPtr(new int[10]);
// или даже так
auto arrayPtr = std::make_unique<int[]>(10);
3. Осторожное использование get()
void riskyCode(std::unique_ptr<Device>& device) {
Device* raw = device.get();
delete raw; // Никогда так не делайте
// Этим «device» теперь указывается на удаленную память
}
Производительность
По сравнению с необработанными указателями у unique_ptr нулевые накладные расходы:
- Нет затрат времени выполнения, то есть размер тот же, что у необработанного указателя.
- Нет снижения производительности при разыменовании.
- Операции перемещения столь же быстрые, как копирование необработанных указателей.
// Сравнение размеров
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));
// Пример кода, где важна производительность
void processLargeData(std::unique_ptr<BigData> data) {
// Нет накладных расходов при доступе к данным
data->process();
// Не требуется ручной очистки
}
Благодаря unique_ptr обеспечивается безопасность автоматического управления памятью без ущерба для производительности. Начните использовать unique_ptr в коде уже сегодня — и в будущем отладка проблем, связанных с памятью, из ежедневной задачи превратится в редкое явление.
Читайте также:
- C++: полное руководство по разделению строк
- C++: полное руководство по вставке в векторах
- Спецификатор constexpr в C++: зачем он нужен и как работает
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: unique_ptr in C++: Complete Guide





