← Назад к вопросам

Как используется Deleter в unique_ptr?

2.8 Senior🔥 151 комментариев
#Умные указатели и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как используется 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 позволяет:

  1. Управлять нестандартным удалением (fclose вместо delete)
  2. Работать с C API безопасно (RAII для malloc/free, fopen/fclose)
  3. Добавлять логику при удалении (логирование, callback)
  4. Реализовать пользовательские стратегии (pool allocation, custom cleanup)
  5. Сохранить 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++ безопасным и эффективным!