Использование пользовательских удалителей (deleter) с shared_ptr и unique_ptr в C++
Как использовать пользовательский удалитель (deleter, делитер) с unique_ptr
и shared_ptr
.
Содержание
- Введение
- Истинное неизвестное лицо умных указателей
- Что такое
std::default_delete
на самом деле? - Способы указания пользовательских удалителей
- Использование пользовательского удалителя с
shared_ptr
- Использование пользовательского удалителя с
unique_ptr
- Хранение пользовательских удалителей
- Ограничения, связанные с пользовательским удалителем
Введение
Зачем и когда нам может понадобиться что-то подобное?
Случай 1: Чтобы иногда полностью удалить объект, нам нужно выполнить какое-то дополнительное действие. Что, если выполнение delete
(которое умные указатели делают автоматически) – не единственное, что нужно сделать перед полным уничтожением объекта.
Случай 2: Мы не можем привязать shared_ptr
или unique_ptr
к объекту, размещенному в стеке, потому что вызов delete
для него приведет к неопределенному поведению.
Случай 3: Сочетание кода на разных языках программирования, например C++ с Obj-C++. Так как для objective-c может потребоваться сложный механизм освобождения для его типов данных, например, вызов CFRelease, для этого нам понадобится пользовательский удалитель.
Случай 4: В C, где, когда вы оборачиваете FILE*
или какую-то структуру в стиле C в free()
, могут быть полезны пользовательские удалители.
и несколько других случаев.
Истинное неизвестное лицо умных указателей
std::unique_ptr
Полный тип std::unique_ptr
имеет второй шаблонный параметр, его удалитель, который по умолчанию имеет тип std::default_delete<T>
.
Что это такое?? Не нужно беспокоиться, мы рассмотрим это вместе :)
template< class T, class Deleter = std::default_delete<T> >
class unique_ptr;
// Управляет одним объектом
template < class T, class Deleter>
class unique_ptr<T[], Deleter>;
// Управляет динамически размещаемым массивом объектов
std::default_delete<T>
– это функциональный объект (или функтор), который вызывает удаление объекта при своем вызове. Это только тип по умолчанию для вызова Deleter
, и его можно заменить пользовательским удалителем.
Вызов выполняется с помощью operator()
объекта Deleter
.
std::shared_ptr
При создании общего указателя вы можете передать в качестве удалителя любую вызываемую сущность (лямбду, функтор) в конструкторе в качестве дополнительного аргумента.
template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );
// Одна из перегрузок конструктора shared_ptr
таким образом, указание пользовательского удалителя для std::shared_ptr
относительно просто.
Когда счетчик ссылок достигает нуля, shared_ptr
использует выражение удаления, т. е. delete ptr
.
Также, начиная с C++17:
// начиная с C++17, shared_ptr можно использовать для управления
// динамически размещаемым массивом, указывая шаблонный аргумент
// с помощью T[N] или T[].
// Итак, вы можете написать
shared_ptr<int[]> myShared(new int[10]);
Что такое std::default_delete
на самом деле?
Это определено в заголовке <memory>
.
template< class T > struct default_delete;
template< class T > struct default_delete<T[]>;
- Неспециализированный
default_delete
используетdelete
для освобождения памяти одного объекта. - Также предоставляется частичная специализация для типов массивов, использующих
delete[]
.
Члены
1. Конструктор – может быть по умолчанию или шаблонным.
constexpr default_delete() noexcept = default; // по умолчанию
template <class U>
default_delete( const default_delete<U>& d ) noexcept; // шаблон
// Создает объект std::default_delete из другого.
// Разрешение перегрузки, если U* неявно преобразуется в T*.
2. operator()
– перегрузка operator()
необходима для возможности вызова структуры/класса как объекта функции (или функтора). В той точке кода, где вызывается этот operator()
, тип должен быть завершен и определен.
Пример 1
{
std::unique_ptr<int> ptr(new int(5));
}
// unique_ptr<int> использует default_delete<int>
Пример 2
{
std::unique_ptr<int[]> ptr(new int[10]);
}
// unique_ptr<int[]> использует default_delete<int[]>
Пример 3
// default_delete можно использовать везде, где нужен функтор удаления
std::vector<int*> v;
for(int n = 0; n < 100; ++n)
v.push_back(new int(n));
std::for_each(v.begin(), v.end(), std::default_delete<int>());
// ↑↑↑↑↑
// Конструирование функционального объекта для вызова
Пример 4
{
std::shared_ptr<int> shared_bad(new int[10]);
}
// деструктор вызывает delete, неопределенное поведение, так как это массив
{
std::shared_ptr<int> shared_good(new int[10], std::default_delete<int[]> ());
} // деструктор вызывает delete[], ок
Пример 5 (действителен только для C++17 и выше)
{
shared_ptr<int[]> shared_best(new int[10]);
}
// деструктор вызывает delete[], офигенно!!
Способы указания пользовательских удалителей
std::function
– требует много памяти (~32 байта! на x64).- Указатель на функцию – просто указатель.
- Функтор без состояния / лямбда без состояния – не требует дополнительной памяти.
- Функтор с состоянием / лямбда с состоянием –
sizeof(функтор или лямбда)
.
Использование пользовательского удалителя с shared_ptr
1. Использование правильного функтора
(пользовательский удалитель для массива требуется только до C++17)
// объявляем функциональный объект
template< typename T >
struct array_deleter
{
void operator ()( T const * p)
{
delete[] p;
}
};
// и используем shared_ptr следующим образом, создавая функциональный объект
std::shared_ptr<int> sp(new int[10], array_deleter<int>());
2. Использование простой лямбды
std::shared_ptr<MyType> sp(new int[10], [](int *p) { delete[] p; });
3. Использование default_delete
(действительно для типов массивов только до C++17)
std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
Примечание.delete ptr
аналогично указанию default_delete<T>{}ptr
.
Использование пользовательского удалителя с unique_ptr
С unique_ptr
немного сложнее. Главное, чтобы тип удалителя был частью типа unique_ptr
.
По умолчанию мы получаем std::default_delete
, вот несколько примеров:
Для класса MyType
class MyType {
// ...
// ...
};
void deleter(MyType*) {
// ...
}
1. std::function
std::unique_ptr<MyType, std::function<void (MyType*)>> u1(new MyType());
// ИЛИ
std::unique_ptr<MyType, decltype(deleter)>> u1(new MyType(), &deleter);
// 2-й аргумент всегда необязателен, так как объект функтора создается по умолчанию
2. Указатель на функцию
std::unique_ptr<MyType, void (*)(MyType *)> u2(new MyType());
3. Функтор без состояния
// функтор без состояния
struct MyTypeDeleterFunctor {
void operator()(MyType* p) {
// ...
}
};
std::unique_ptr<MyType, MyTypeDeleterFunctor>u3(new MyType());
4.
void close_file(std::FILE* fp) { std::fclose(fp); }
5. Способ с лямбдой
auto deleter = [](MyType*){ ... }
std::unique_ptr<MyType, decltype(deleter)>> u1(new MyType(), deleter);
Хранение пользовательских удалителей
Для shared_ptr
Когда вы используете пользовательский удалитель, это не влияет на размер вашего типа shared_ptr
. Если помните, размер shared_ptr
должен быть примерно в 2 раза больше sizeof(ptr)
, так где же прячется этот удалитель?
Как мы знаем, shared_ptr
состоит из двух вещей: указателя на объект и указателя на управляющий блок (который содержит счетчик ссылок). Внутри структуры управляющего блока shared_ptr
есть место для пользовательских делитера и аллокатора.
Для unique_ptr
unique_ptr
маленький и эффективный; размер равен одному указателю, так где же в этом случае скрывается пользовательский удалитель?
Удалитель является частью типа unique_ptr
. И поскольку функтор/лямбда не имеет состояния, его тип полностью кодирует всё, что нужно знать, без какого-либо влияния на размер. Использование указателя на функцию занимает размер одного указателя, а std::function
требует еще большего размера.
В
shared_ptr
удалитель хранится всегда, это стирает тип удалителя, что может быть полезно в API. Недостатки использованияshared_ptr
вместоunique_ptr
включают более высокие затраты памяти для хранения удалителя и затраты производительности на поддержку счетчика ссылок.
Интересный факт: размер weak_ptr
такой же, как и у shared_ptr
. Слабый указатель указывает на тот же управляющий блок, что и общий указатель. Когда weak_ptr
создается, уничтожается или копируется, производятся манипуляции со вторым счетчиком ссылок (счетчиком слабых ссылок). Счетчик слабых ссылок связан с освобождением хранилища объекта.
Ограничения, связанные с пользовательским удалителем
Невозможно использовать в make_shared
с shared_ptr
К сожалению, вы можете передать пользовательский удалитель только в конструкторе shared_ptr
, а в make_shared
нет возможности его использовать. Это может быть небольшим недостатком.
Можно использовать allocate_shared
и пользовательские аллокатор и делитер, но это слишком сложно, чтобы описывать его в этой статье.
Невозможно использовать в make_unique
с unique_ptr
Так же, как и в случае с shared_ptr
, вы можете передать пользовательский удалитель только в конструкторе unique_ptr
и, следовательно, вы не можете использовать make_unique
.