Использование пользовательских удалителей (deleter) с shared_ptr и unique_ptr в C++

Добавлено 1 января 2022 в 12:40

Как использовать пользовательский удалитель (deleter, делитер) с unique_ptr и shared_ptr.

Использование пользовательских удалителей (deleter) с shared_ptr и unique_ptr в C++

Содержание

Введение

Зачем и когда нам может понадобиться что-то подобное?

Случай 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[]>;
  1. Неспециализированный default_delete использует delete для освобождения памяти одного объекта.
  2. Также предоставляется частичная специализация для типов массивов, использующих 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[], офигенно!!

Способы указания пользовательских удалителей

  1. std::function – требует много памяти (~32 байта! на x64).
  2. Указатель на функцию – просто указатель.
  3. Функтор без состояния / лямбда без состояния – не требует дополнительной памяти.
  4. Функтор с состоянием / лямбда с состоянием – 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.

Теги

C++ / CppC++17Custom deleter / Пользовательский удалительstd::shared_ptrstd::unique_ptrПрограммированиеСемантика перемещенияУмные указатели

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.