Управление памятью на C++ не должно быть источником стресса. С тех пор как в C++11 появился unique_ptr, изменилась работа с динамической памятью, так что при минимальных накладных расходах на производительность утечки памяти стали практически невозможны.

Посмотрим, как это осуществляется на практике.

Чем отличается unique_ptr?

unique_ptr владеет тем, на что указывает. Когда же unique_ptr уничтожается, управляемый им объект автоматически удаляется. Это означает:

  1. Отсутствие ручного управления памятью.
  2. Отсутствие утечек памяти.
  3. Четкая семантика владения.
  4. Нулевые затраты времени выполнения по сравнению с необработанными указателями.

Вот простой пример:

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?

  1. Им предотвращаются утечки памяти в аргументах функции.
  2. Им гарантируется безопасность исключений.
  3. Он лаконичнее и удобнее для восприятия.

Работа с 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 нулевые накладные расходы:

  1. Нет затрат времени выполнения, то есть размер тот же, что у необработанного указателя.
  2. Нет снижения производительности при разыменовании.
  3. Операции перемещения столь же быстрые, как копирование необработанных указателей.
// Сравнение размеров
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));

// Пример кода, где важна производительность
void processLargeData(std::unique_ptr<BigData> data) {
// Нет накладных расходов при доступе к данным
data->process();
// Не требуется ручной очистки
}

Благодаря unique_ptr обеспечивается безопасность автоматического управления памятью без ущерба для производительности. Начните использовать unique_ptr в коде уже сегодня  —  и в будущем отладка проблем, связанных с памятью, из ежедневной задачи превратится в редкое явление.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: unique_ptr in C++: Complete Guide

Предыдущая статьяПочему шифрование и дешифрование данных необходимо для безопасности приложений и бэкенд-систем