M.6 – std::unique_ptr

Добавлено 18 сентября 2021 в 18:30

В начале главы мы обсудили, как использование указателей может в некоторых ситуациях приводить к ошибкам и утечкам памяти. Например, это может произойти, когда функция возвращается преждевременно или генерирует исключение, а указатель не удаляется должным образом.

#include <iostream>

void someFunction()
{
    auto *ptr{ new Resource() };

    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;

    if (x == 0)
        throw 0; // функция возвращается раньше времени, и ptr не будет удален!

    // здесь что-то делаем с ptr

    delete ptr;
}

Теперь, когда мы рассмотрели основы семантики перемещения, мы можем вернуться к теме классов умных указателей. Напоминаем, что умный указатель – это класс, который управляет динамически размещаемым объектом. Хотя умные указатели могут предлагать и другие функции, определяющей характеристикой умного указателя является то, что он управляет динамически размещаемым ресурсом и обеспечивает правильную очистку динамически размещенного объекта в соответствующий момент (обычно, когда умный указатель выходит за пределы области видимости).

Из-за этого умные указатели никогда не должны сами размещаться динамически (в противном случае существует риск того, что умный указатель может быть неправильно освобожден, что означает, что и объект, которым он владеет, не будет освобожден, что приведет к утечке памяти). Всегда размещая умные указатели в стеке (в качестве локальных переменных или членов композиции класса), мы гарантируем, что умный указатель правильно выйдет за пределы области видимости, когда функция или объект, внутри которого он содержится, закончится, что гарантирует, что объект, которым владеет умный указатель, будет освобожден правильно.

Стандартная библиотека C++11 поставляется с 4 классами умных указателей: std::auto_ptr (который вам не следует использовать – он удаляется в C++17), std::unique_ptr, std::shared_ptr и std::weak_ptr. std::unique_ptr – это, безусловно, наиболее часто используемый класс умных указателей, поэтому мы рассмотрим его в первую очередь. В следующих уроках мы рассмотрим std::shared_ptr и std::weak_ptr.

std::unique_ptr

std::unique_ptr – это замена для std::auto_ptr в C++11. Его следует использовать для управления любым динамически размещаемым объектом, который не используется несколькими объектами. То есть std::unique_ptr должен полностью владеть объектом, которым он управляет, а не разделять это владение с другими классами. std::unique_ptr находится в заголовке <memory>.

Давайте посмотрим на простой пример умного указателя:

#include <iostream>
#include <memory> // для std::unique_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    // размещаем объект Resource и оставляем его во владении std::unique_ptr
    std::unique_ptr<Resource> res{ new Resource() };

    return 0;
} // здесь res выходит из области видимости, а выделенный Resource уничтожается

Поскольку std::unique_ptr размещается здесь в стеке, он гарантированно выйдет за пределы области видимости, и когда это произойдет, он удалит объект Resource, которым управляет.

В отличие от std::auto_ptr, std::unique_ptr правильно реализует семантику перемещения.

#include <iostream>
#include <memory>  // для std::unique_ptr
#include <utility> // для std::move

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};

int main()
{
    std::unique_ptr<Resource> res1{ new Resource{} }; // Resource создается здесь
    std::unique_ptr<Resource> res2{}; // Начинаем как nullptr

    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");

    // res2 = res1; // Не компилируется: копирование отключено
    res2 = std::move(res1); // res2 принимает владение, res1 обнуляется

    std::cout << "Ownership transferred\n";

    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");

    return 0;
} // Resource здесь уничтожается, когда res2 выходит за пределы области видимости

Этот код печатает:

Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

Поскольку std::unique_ptr разработан с учетом семантики перемещения, копирующая инициализация и присваивание копированием отключены. Если вы хотите передать содержимое, управляемое std::unique_ptr, вы должны использовать семантику перемещения. В приведенной выше программе мы выполняем это с помощью std::move (который преобразует res1 в r-значение, что вызывает присваивание перемещением вместо присваивания копированием).

Доступ к управляемому объекту

std::unique_ptr имеет перегруженные operator* и operator->, которые можно использовать для возврата управляемого ресурса. operator* возвращает ссылку на управляемый ресурс, а operator-> возвращает указатель.

Помните, что std::unique_ptr может не всегда управлять объектом – либо потому, что он был создан пустым (с использованием конструктора по умолчанию или с передачей nullptr в качестве параметра), либо потому, что ресурс, которым он управлял, был перемещен в другой std::unique_ptr. Поэтому, прежде чем использовать любой из этих операторов, мы должны проверить, действительно ли std::unique_ptr имеет ресурс. К счастью, это просто: std::unique_ptr имеет преобразование в bool, которое возвращает true, если std::unique_ptr управляет ресурсом.

Вот пример этого:

#include <iostream>
#include <memory> // для std::unique_ptr

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource\n";
        return out;
    }
};

int main()
{
    std::unique_ptr<Resource> res{ new Resource{} };

    if (res) // используем неявное приведение к bool, чтобы убедиться, что res содержит Resource
        std::cout << *res << '\n'; // выводим Resource, которым владеет res

    return 0;
}

Этот код печатает:

Resource acquired
I am a resource
Resource destroyed

В приведенной выше программе мы используем перегруженный operator*, чтобы получить объект Resource, принадлежащий std::unique_ptr res, который затем отправляем в std::cout для печати.

std::unique_ptr и массивы

В отличие от std::auto_ptr, std::unique_ptr достаточно умен, чтобы знать, использовать ли удаление одного объекта или удаление массива, поэтому std::unique_ptr можно использовать как с одиночными объектами, так и с массивами.

Однако std::array или std::vector (или std::string) почти всегда лучше, чем использование std::unique_ptr с фиксированным массивом, динамическим массивом или строкой в ​​стиле C.

Лучшая практика


Используйте std::array, std::vector или std::string вместо умного указателя, управляющего фиксированным массивом, динамическим массивом или строкой в ​​стиле C.

std::make_unique

В C++14 есть дополнительная функция std::make_unique(). Эта шаблонная функция создает объект шаблонного типа и инициализирует его аргументами, переданными в эту функцию.

#include <memory> // для std::unique_ptr и std::make_unique
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    Fraction(int numerator = 0, int denominator = 1) :
        m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << '/' << f1.m_denominator;
        return out;
    }
};


int main()
{
    // Создание одиночной динамически размещаемой дроби Fraction
    // с числителем 3 и знаменателем 5.
    // Здесь мы также можем использовать автоматический вывод типа
    // для простоты кода.
    auto f1{ std::make_unique<Fraction>(3, 5) };
    std::cout << *f1 << '\n';

    // Создаем динамически размещаемый массив дробей Fraction длиной 4
    auto f2{ std::make_unique<Fraction[]>(4) };
    std::cout << f2[0] << '\n';

    return 0;
}

Приведенный выше код печатает:

3/5
0/1

Использование std::make_unique() необязательно, но рекомендуется вместо самостоятельного создания std::unique_ptr. Это связано с тем, что код, использующий std::make_unique, проще, а также требует меньшего набора текста (при использовании с автоматическим выводом типа). Кроме того, она решает проблему безопасности исключений, которая может возникнуть из-за того, что C++ оставляет неопределенным порядок вычисления аргументов функции.

Правило


Используйте std::make_unique() вместо создания std::unique_ptr и использования new самостоятельно.

Более подробно о проблеме безопасности исключений

Для тех, кто интересуется, что такое «проблема безопасности исключений», упомянутая выше, вот ее описание.

Рассмотрим такое выражение:

some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception());

Компилятору предоставляется большая гибкость в том, как он обрабатывает этот вызов. Он может создать новый объект T, затем вызвать function_that_can_throw_exception(), а затем создать std::unique_ptr, который управляет динамически размещенным объектом T. Если function_that_can_throw_exception() выдает исключение, то размещенный объект T не будет освобожден, потому что умный указатель, предназначенный для выполнения этого освобождения, еще не создан. Это приводит к утечке объекта T.

std::make_unique() не страдает от этой проблемы, потому что создание объекта T и создание std::unique_ptr происходит внутри функции std::make_unique(), где нет неоднозначности в порядке выполнения.

Возврат std::unique_ptr из функции

std::unique_ptr можно безопасно вернуть из функции по значению:

std::unique_ptr<Resource> createResource()
{
     return std::make_unique<Resource>();
}

int main()
{
    auto ptr{ createResource() };

    // что-то здесь делаем

    return 0;
}

В приведенном выше коде createResource() возвращает std::unique_ptr по значению. Если это значение ничему не присвоено, временное возвращаемое значение выйдет за пределы области видимости, и Resource будет очищен. Если оно присваивается чему-то (как показано в main()) в C++14 или более ранней версии, семантика перемещения будет использоваться для передачи объекта Resource из возвращаемого значения в объект, которому выполняется присваивание (в приведенном выше примере – ptr), а в C++17 или новее перемещение возвращаемого значения будет опущено. Это делает возврат ресурса с помощью std::unique_ptr намного безопаснее, чем возврат простых указателей!

В общем, вы не должны возвращать std::unique_ptr по указателю (никогда) или по ссылке (если у вас нет конкретной веской причины для этого).

Передача std::unique_ptr в функцию

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

#include <memory>  // для std::unique_ptr
#include <utility> // для std::move

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource\n";
        return out;
    }
};

void takeOwnership(std::unique_ptr<Resource> res)
{
     if (res)
          std::cout << *res << '\n';
} // Resource здесь уничтожается

int main()
{
    auto ptr{ std::make_unique<Resource>() };

//    takeOwnership(ptr); // Это не работает, необходимо использовать семантику перемещения
    takeOwnership(std::move(ptr)); // ok: используем семантику перемещения

    std::cout << "Ending program\n";

    return 0;
}

Данная программа печатает:

Resource acquired
I am a resource
Resource destroyed
Ending program

Обратите внимание, что в этом случае владение объектом Resource было передано функции takeOwnership(), поэтому объект Resource был уничтожен в конце takeOwnership(), а не в конце main().

Однако в большинстве случаев вы не хотите, чтобы функция забирала владение ресурсом. Хотя вы можете передать std::unique_ptr по ссылке (что позволит функции использовать объект, не принимая на себя владение), вы должны делать это только тогда, когда вызываемая функция может изменить управляемый объект.

Вместо этого лучше просто передать сам ресурс (по указателю или по ссылке, в зависимости от того, является ли nullptr допустимым аргументом). Это позволяет функции не зависеть от того, как вызывающий управляет своими ресурсами. Чтобы получить простой указатель на ресурс из std::unique_ptr, вы можете использовать функцию-член get():

#include <memory> // для std::unique_ptr
#include <iostream>

class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }

    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource\n";
        return out;
    }
};

// Эта функция использует только ресурс, поэтому мы принимаем
// указатель на ресурс, а не ссылку на весь std::unique_ptr<Resource>
void useResource(Resource *res)
{
    if (res)
        std::cout << *res << '\n';
}

int main()
{
    auto ptr{ std::make_unique<Resource>() };

    // обратите внимание: здесь используется get()
    // для получения указателя на объект Resource
    useResource(ptr.get()); 

    std::cout << "Ending program\n";

    return 0;
} // Resource здесь уничтожается

Показанная выше программа печатает:

Resource acquired
I am a resource
Ending program
Resource destroyed

std::unique_ptr и классы

Вы, конечно, можете использовать std::unique_ptr в качестве члена композиции вашего класса. Таким образом, вам не нужно беспокоиться о том, чтобы деструктор класса освобождал динамическую память, поскольку std::unique_ptr будет автоматически уничтожен при уничтожении объекта класса. Однако обратите внимание, что если объект вашего класса размещается динамически, сам объект рискует быть освобожден некорректным образом, и в этом случае не поможет даже умный указатель.

Неправильное использование std::unique_ptr

Есть два простых способа неправильного использования умных указателей std::unique_ptr, и обоих легко избежать. Во-первых, не позволяйте нескольким классам управлять одним и тем же ресурсом. Например:

Resource *res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
std::unique_ptr<Resource> res2{ res };

Хотя синтаксически это допустимо, конечным результатом будет то, что и res1, и res2 попытаются удалить Resource, что приведет к неопределенному поведению.

Во-вторых, не удаляйте ресурс вручную, в обход std::unique_ptr.

Resource *res{ new Resource() };
std::unique_ptr<Resource> res1{ res };
delete res;

Если вы это сделаете, std::unique_ptr попытается удалить уже удаленный ресурс, что снова приведет к неопределенному поведению.

Обратите внимание, что std::make_unique() предотвращает непреднамеренное выполнение обоих перечисленных случаев.

Небольшой тест

Вопрос 1

Преобразуйте следующую программу с использования обычного указателя на использование std::unique_ptr, где это необходимо:

#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    Fraction(int numerator = 0, int denominator = 1) :
        m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << '/' << f1.m_denominator;
        return out;
    }
};

void printFraction(const Fraction* ptr)
{
    if (ptr)
        std::cout << *ptr << '\n';
}

int main()
{
    auto *ptr{ new Fraction{ 3, 5 } };

    printFraction(ptr);

    delete ptr;

    return 0;
}

#include <memory> // для std::unique_ptr
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    Fraction(int numerator = 0, int denominator = 1) :
        m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << '/' << f1.m_denominator;
        return out;
    }
};

// Эта функция использует объект Fraction,
// поэтому мы просто передаем сам Fraction.
// Таким образом, нам не нужно беспокоиться о том, какой умный указатель
// (если он есть) может использоваться вызывающей функцией.
void printFraction(const Fraction* ptr)
{
    if (ptr)
        std::cout << *ptr << '\n';
}

int main()
{
    auto ptr{ std::make_unique<Fraction>(3, 5) };

    printFraction(ptr.get());

    return 0;
}

Теги

C++ / CppLearnCppstd::make_uniquestd::unique_ptrSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихОбучениеПрограммированиеСемантика перемещенияУмные указатели

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

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