Денис Злобин

Всё про std::unique_ptr

std::unique_ptr — это умный указатель, который владеет и управляет объектом через обычный указатель. Управляемый объект автоматически уничтожается, когда уничтожается сам unique_ptr, либо когда unique_ptr присваивается другой указатель через operator= или вызывается метод reset().

unique_ptr нельзя копировать: в его реализации удалены (помечены как delete) конструктор копирования и оператор копирующего присваивания. Однако неконстантный unique_ptr можно перемещать.

Существует две версии unique_ptr:

  1. Для управления одним объектом (выделенным с помощью new).
  2. Для управления динамическим массивом объектов (выделенным с помощью new[]).

Удаление объекта во владении выполняется либо с помощью удалителя по умолчанию, либо с помощью пользовательского удалителя.

По умолчанию размер unique_ptr равен размеру обычного указателя на данной платформе. Однако, если используется пользовательский удалитель, размер unique_ptr может увеличиться.

Как создать std::unique_ptr?

В этой статье я рассмотрю все способы создания std::unique_ptr.

Способ 1. С помощью вызова конструктора

Этот способ появился в C++11: выделяем память под объект и передаём указатель в конструктор std::unique_ptr.

const std::unique_ptr<int> v1(new int(1));

Способ 2. Используем std::make_unique

Этот способ появился в C++14 для защиты от утечки: до C++17 в выражении вида f(std::unique_ptr<int>(new int), mayThrow()) исключение могло прилететь между new и вызовом конструктора — память утекала. В C++17 порядок вычисления аргументов ужесточили, но std::make_unique до сих пор один из самых распространённых способов создания std::unique_ptr. Под капотом он полностью идентичен Способу 1.

const auto v2 = std::make_unique<int>(2);

Способ 3. Используем std::make_unique_for_overwrite

Этот способ появился в C++20. std::make_unique_for_overwrite выделяет память под ресурс, но ресурс при этом default-initialized

👉 https://en.cppreference.com/w/cpp/language/default_initialization#Notes

В примере ниже массив int не инициализируется:

const auto v3 = std::make_unique_for_overwrite<int[]>(2);

Способ 4. Используем пользовательский удалитель

Хотя способ появился ещё в C++11, он до сих пор остаётся одной из незаслуженно обделённых вниманием возможностей C++. Вспомните про него, когда вам придёт в голову мысль написать RAII-обёртку для C-шных указателей на ресурс :)

void customDeleter(int* value)
{
    std::cout << "Custom deleter for value: " << *value << '\n';
    delete value;
}

using custom_deleter_t = std::unique_ptr<int, decltype(&customDeleter)>;
custom_deleter_t v4(new int(4), &customDeleter);

Способ 5. Перемещаем ресурс из другого std::unique_ptr

Напоминаю: перемещать можно только неконстантный std::unique_ptr.

custom_deleter_t v5(new int(5), &customDeleter);
v5 = std::move(v4);

Способ 6. Заменяем ресурс во владении

На самом деле это просто Способ 5 под капотом :)

custom_deleter_t v6(new int(6), &customDeleter);
v6.reset(v5.release());

Способ 7. Вы ещё не устали? :)

std::unique_ptr можно возвращать как результат выполнения функции: при return локальный unique_ptr неявно перемещается, а NRVO может убрать и само перемещение.

std::unique_ptr<int> makeInt(int value)
{
    std::unique_ptr<int> result;
    // Тяжёлые подготовительные операции для создания объекта :)
    value /= 2;
    result = std::make_unique<int>(value);
    return result;
}

const auto v7 = makeInt(14);

Пользовательские удалители в std::unique_ptr

Рассмотрим объявление std::unique_ptr:

template<
    class T,
    class Deleter = std::default_delete<T>
> class unique_ptr;

std::unique_ptr — это шаблон, параметризуемый:

  1. типом объекта, которым он владеет (первый параметр шаблона),
  2. удалителем (второй параметр шаблона).

Удалитель вызывается из деструктора std::unique_ptr и используется для освобождения памяти, выделенной под объект во владении. В качестве удалителя может выступать функциональный объект, lvalue-ссылка на функцию или сама функция. По умолчанию используется std::default_delete, который вызывает delete (или delete[] для массивов).

Теперь, разобравшись с тем, что такое пользовательский удалитель, посмотрим, как он влияет на размер std::unique_ptr.

Все примеры запускались на архитектуре x86_64.

Удалитель по умолчанию

Размер std::unique_ptr составляет 8 байт:

const std::unique_ptr<int> v1(new int(1));

Свободная функция в качестве удалителя

Свободная функция увеличивает размер std::unique_ptr на размер указателя (на функцию). Итоговый размер — 16 байт:

void customDeleter(int* value)
{
    std::cout << "Free function deleter for value: " << *value << '\n';
    delete value;
}

const std::unique_ptr<int, decltype(&customDeleter)> v2(new int(2), &customDeleter);

Лямбда без состояния

Лямбда без состояния не увеличивает размер std::unique_ptr. Размер остаётся 8 байт:

auto lambda = [](int* value) {
    std::cout << "Stateless lambda deleter for value: " << *value << '\n';
    delete value;
};

const std::unique_ptr<int, decltype(lambda)> v3(new int(3), lambda);

Лямбда с состоянием

Лямбда с состоянием увеличивает размер std::unique_ptr. В данном случае размер равен 16 байт.

constexpr bool s{false};
auto lambda = [s](int* value) {
    std::cout << "Lambda with state deleter for value: " << *value << '\n';
    delete value;
};

const std::unique_ptr<int, decltype(lambda)> v4(new int(4), lambda);

std::function в качестве удалителя

Использование std::function драматически увеличивает размер std::unique_ptr — до 40 байт:

std::function<void(int*)> function{customDeleter};
const std::unique_ptr<int, decltype(function)> v5(new int(5), function);