Как используется Deleter в unique_ptr?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как используется Deleter в unique_ptr?
Deleter в unique_ptr - это фундаментальная концепция RAII в C++. Позволяет контролировать, как именно удаляется объект. Давайте разберемся детально.
Краткий ответ
Deleter - это callable object (функция, функтор, лямбда), которая вызывается при уничтожении unique_ptr для освобождения ресурса. Это позволяет управлять нестандартным удалением объектов.
1. Стандартный deleter по умолчанию
Простой случай - обычный new/delete:
#include <memory>
// Это эквивалентно:
std::unique_ptr<int> ptr1(new int(42));
// Deleter по умолчанию - std::default_delete<int>
// Который просто делает: delete ptr;
ptr1.reset(); // Вызывает operator delete(ptr1.get())
// Это работает автоматически
class Widget {
public:
~Widget() { std::cout << "Widget destroyed\n"; }
};
std::unique_ptr<Widget> widget(new Widget());
// При выходе из scope вызывается ~Widget()
Для массивов:
// Специализация unique_ptr для массивов
std::unique_ptr<int[]> arr(new int[100]);
// Deleter использует delete[] (не delete!)
arr.reset(); // Вызывает operator delete[]
2. Custom deleter - основная фишка
Зачем нужен custom deleter:
// Пример: работа с FILE* из C
FILE* file = fopen("data.txt", "r");
// ...
fclose(file); // Должны вызвать fclose, не delete!
// Проблема: unique_ptr по умолчанию вызывает delete
// Но FILE* был выделен через malloc-подобную функцию!
// Решение: использовать custom deleter
Вариант 1: Функция как deleter
#include <cstdio>
#include <memory>
// Функция для удаления FILE*
void close_file(FILE* file) {
if (file) {
fclose(file);
std::cout << "File closed\n";
}
}
int main() {
// Указываем тип deleter в шаблоне
FILE* raw_file = fopen("data.txt", "r");
// Создаем unique_ptr с custom deleter
std::unique_ptr<FILE, decltype(&close_file)> file(raw_file, &close_file);
// При выходе из scope вызовется close_file()
// Вместо delete file
return 0;
}
Вариант 2: Функтор как deleter
#include <memory>
struct FileDeleter {
void operator()(FILE* file) const {
if (file) {
fclose(file);
std::cout << "Functor: File closed\n";
}
}
};
int main() {
FILE* raw_file = fopen("data.txt", "r");
// Функтор как deleter
std::unique_ptr<FILE, FileDeleter> file(raw_file);
return 0;
}
Вариант 3: Лямбда как deleter (C++14+)
#include <memory>
#include <cstdio>
int main() {
FILE* raw_file = fopen("data.txt", "r");
// Лямбда с capture как deleter
auto file = std::unique_ptr<FILE, decltype([](FILE* f) {
if (f) fclose(f);
})>(raw_file);
// Или с захватом переменных
int count = 0;
auto ptr = std::unique_ptr<FILE, decltype([&](FILE* f) {
if (f) {
fclose(f);
count++; // Можем использовать захватанные переменные
}
})>(raw_file);
return 0;
}
3. Практический пример - работа с API C
Проблема: утечки памяти с C API
#include <sqlite3>
// Опасно:
int bad_example() {
sqlite3* db;
if (sqlite3_open(":memory:", &db) != SQLITE_OK) {
return -1; // БЕЗ sqlite3_close(db)!
// Утечка памяти!
}
sqlite3_close(db);
return 0;
}
Решение с unique_ptr:
#include <sqlite3>
#include <memory>
int good_example() {
// Определяем deleter
auto db_deleter = [](sqlite3* db) {
if (db) sqlite3_close(db);
};
sqlite3* raw_db = nullptr;
if (sqlite3_open(":memory:", &raw_db) != SQLITE_OK) {
return -1; // raw_db не выделена, нет утечки
}
// Oборачиваем в unique_ptr
std::unique_ptr<sqlite3, decltype(db_deleter)> db(raw_db, db_deleter);
// Используем db->...
// При выходе из scope автоматически вызовется sqlite3_close
return 0;
}
// Или создаём helper функцию
std::unique_ptr<sqlite3, decltype(&sqlite3_close)>
open_database(const char* path) {
sqlite3* db = nullptr;
if (sqlite3_open(path, &db) != SQLITE_OK) {
throw std::runtime_error(sqlite3_errmsg(db));
}
return std::unique_ptr<sqlite3, decltype(&sqlite3_close)>(db, &sqlite3_close);
}
int main() {
auto db = open_database(":memory:");
// Используем db
// Автоматически закроется при выходе
}
4. Структура unique_ptr с deleter
Как это устроено внутри:
// Упрощенная реализация unique_ptr
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:
T* ptr;
[[no_unique_address]] Deleter deleter; // Пустая оптимизация!
public:
unique_ptr(T* p, Deleter d) : ptr(p), deleter(d) {}
~unique_ptr() {
if (ptr) {
deleter(ptr); // Вызываем deleter
}
}
void reset(T* new_ptr = nullptr) {
if (ptr) {
deleter(ptr); // Вызываем deleter
}
ptr = new_ptr;
}
};
// Ключевой момент: [[no_unique_address]]
// Если Deleter - функтор с пустым телом, он не занимает место
struct EmptyDeleter {
void operator()(int*) const {}
};
std::unique_ptr<int, EmptyDeleter> ptr;
// sizeof(ptr) == sizeof(int*), хотя содержит deleter!
5. Stateless vs Stateful deleters
Stateless deleter (без состояния):
// Функции - всегда stateless
void my_delete(int* p) { delete p; }
std::unique_ptr<int, decltype(&my_delete)> ptr1(new int, &my_delete);
// sizeof(ptr1) == sizeof(int*) ← Deleter НЕ занимает место!
// Пустой функтор - stateless
struct Deleter {
void operator()(int* p) const { delete p; }
};
std::unique_ptr<int, Deleter> ptr2(new int);
// sizeof(ptr2) == sizeof(int*) ← Оптимизирован!
Stateful deleter (с состоянием):
// Функтор с членами - stateful
struct CustomDeleter {
int call_count = 0;
void operator()(int* p) const {
delete p;
call_count++; // Отслеживаем вызовы
}
};
std::unique_ptr<int, CustomDeleter> ptr(new int, CustomDeleter());
// sizeof(ptr) == sizeof(int*) + sizeof(int) ← Deleter занимает место
// Лямбда с capture - stateful
auto deleter = [log_file](int* p) {
std::cerr << log_file << ": deleting\n";
delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int, deleter);
// sizeof(ptr) > sizeof(int*) ← Захватанные переменные занимают место
6. Advanced примеры
Пример 1: Удаление с callback
#include <memory>
#include <iostream>
class Resource {
public:
~Resource() { std::cout << "Resource destroyed\n"; }
};
class ResourceManager {
public:
std::unique_ptr<Resource, std::function<void(Resource*)>>
create_resource() {
auto res = new Resource();
return std::unique_ptr<Resource, std::function<void(Resource*)>>(
res,
[this](Resource* r) {
std::cout << "Cleanup via manager\n";
delete r;
on_resource_deleted(); // Callback
}
);
}
private:
void on_resource_deleted() {
std::cout << "Resource was deleted\n";
}
};
int main() {
ResourceManager manager;
auto res = manager.create_resource();
// При выходе из scope вызовется callback
}
Пример 2: Pool allocation
#include <memory>
#include <vector>
class MemoryPool {
std::vector<int*> available;
public:
std::unique_ptr<int, std::function<void(int*)>> allocate() {
int* ptr;
if (!available.empty()) {
ptr = available.back();
available.pop_back();
} else {
ptr = new int();
}
return std::unique_ptr<int, std::function<void(int*)>>(
ptr,
[this](int* p) {
available.push_back(p); // Возвращаем в pool
}
);
}
};
int main() {
MemoryPool pool;
{
auto ptr1 = pool.allocate();
auto ptr2 = pool.allocate();
// При выходе вернутся в pool, не удалятся
}
}
7. std::function<void(T*)> vs custom deleter
Когда что использовать:
// Вариант 1: Конкретный тип deleter (рекомендуется)
void my_deleter(int* p) { delete p; }
std::unique_ptr<int, decltype(&my_deleter)> ptr1(new int, &my_deleter);
// ✓ Быстро, нет overhead
// ✓ Тип известен в compile-time
// ✗ Нужно писать тип в шаблоне
// Вариант 2: std::function (для динамических deleters)
std::unique_ptr<int, std::function<void(int*)>>
ptr2(new int, [](int* p) { delete p; });
// ✓ Гибко, можно менять deleter
// ✓ Type-erased
// ✗ Небольшой overhead (8-16 байт)
// Практическое использование:
std::vector<std::unique_ptr<int, std::function<void(int*)>>> ptrs;
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
ptrs.push_back({new int, [](int* p) { std::cout << "delete\n"; delete p; }});
} else {
ptrs.push_back({new int, [](int* p) { std::cout << "free\n"; free(p); }});
}
}
8. Best Practices
✓ Правила:
// 1. Используй default delete для обычного new/delete
std::unique_ptr<Widget> widget(new Widget());
// 2. Используй decltype для custom deleters
void custom_deleter(Resource* r);
std::unique_ptr<Resource, decltype(&custom_deleter)> res(
new Resource(), &custom_deleter
);
// 3. Для lambda deleters в C++14+
auto ptr = std::unique_ptr<int, decltype([](int* p) { delete p; })>(
new int(42)
);
// 4. Создавай helper функции
auto make_database(const char* path) {
return std::unique_ptr<sqlite3, decltype(&sqlite3_close)>(
open_db(path), &sqlite3_close
);
}
// 5. Избегай std::function если производительность критична
// std::function имеет overhead
// 6. Используй [[no_unique_address]] для пустых deleters (C++20)
template<typename T, typename Deleter>
class MyPtr {
T* ptr;
[[no_unique_address]] Deleter deleter; // Не занимает место если пуст
};
Заключение
Deleter в unique_ptr позволяет:
- Управлять нестандартным удалением (fclose вместо delete)
- Работать с C API безопасно (RAII для malloc/free, fopen/fclose)
- Добавлять логику при удалении (логирование, callback)
- Реализовать пользовательские стратегии (pool allocation, custom cleanup)
- Сохранить zero-cost abstraction (пустой deleter = нет overhead)
Главный паттерн:
// Для каждого C resource создавай wrapper с unique_ptr
// FILE* → std::unique_ptr<FILE, decltype(&fclose)>
// sqlite3* → std::unique_ptr<sqlite3, decltype(&sqlite3_close)>
// libpng → std::unique_ptr<png_struct, decltype(&png_destroy_read_struct)>
// Это гарантирует отсутствие утечек в любых сценариях
// включая исключения!
Deleter - это фундамент RAII паттерна, делающий C++ безопасным и эффективным!