M.6 – std::unique_ptr
В начале главы мы обсудили, как использование указателей может в некоторых ситуациях приводить к ошибкам и утечкам памяти. Например, это может произойти, когда функция возвращается преждевременно или генерирует исключение, а указатель не удаляется должным образом.
#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; }