C++17 STL Стандартная библиотека шаблонов - Яцек Галовиц
Шрифт:
Интервал:
Закладка:
В этом разделе мы рассмотрим unique_ptr и способы его использования.
Как это делается
В этом примере мы напишем программу, которая покажет, как unique_ptr работает с памятью путем создания пользовательского типа, добавляющего некие отладочные сообщения при создании и разрушении объекта. Затем поработаем с уникальными указателями, управляя экземплярами этого типа, для которых память выделяется динамически.
1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен std:
#include <iostream>
#include <memory>
using namespace std;
2. Мы реализуем небольшой класс для объекта, которым будем управлять с помощью unique_ptr. Его конструктор и деструктор станут выводить сообщения на консоль; это поможет узнать о том, что объект был на самом деле удален.
class Foo
{
public:
string name;
Foo(string n)
: name{move(n)}
{ cout << "CTOR " << name << 'n'; }
~Foo() { cout << "DTOR " << name << 'n'; }
};
3. Попробуем реализовать функцию, принимающую в качестве аргументов уникальные указатели, чтобы увидеть, какие ограничения она имеет. Она обрабатывает элемент типа Foo, выводя его название. Обратите внимание: хотя уникальные указатели являются умными, работают без лишних издержек и удобны в использовании, они все еще могут иметь значение null. Это значит, что их все еще нужно проверять перед разыменованием.
void process_item(unique_ptr<Foo> p)
{
if (!p) { return; }
cout << "Processing " << p->name << 'n';
}
4. В функции main откроем еще одну область видимости, создадим два объекта типа Foo в куче и будем управлять ими обоими с помощью уникальных указателей. Создадим первый объект в куче явно, используя оператор new, а затем поместим его в конструктор переменной типа unique_ptr<Foo>, p1. Создадим уникальный указатель p2 путем вызова make_unique<Foo> с аргументами, которые в противном случае передали бы конструктору класса Foo. Этот способ более элегантен, поскольку можно воспользоваться автоматическим определением типов и при первом обращении к объекту он уже будет управляться unique_ptr:
int main()
{
{
unique_ptr<Foo> p1 {new Foo{"foo"}};
auto p2 (make_unique<Foo>("bar"));
}
5. После того как мы покинем область видимости, оба объекта будут разрушены и их память вернется в кучу. Взглянем на функцию process_item и узнаем, как теперь ею пользоваться вкупе с unique_ptr. Если мы создадим новый экземпляр типа Foo, управляемый unique_ptr в вызове функции, то его время жизни будет ограничено областью видимости функции. Когда функция process_item отработает, объект будет уничтожен:
process_item(make_unique<Foo>("foo1"));
6. Если мы хотим вызвать process_item для объекта, который существовал еще до вызова, нужно передать право владения, поскольку данная функция принимает unique_ptr по значению; это значит, что его вызов создаст копию. Но unique_ptr нельзя скопировать, его можно только переместить. Создадим еще два объекта типа Foo и переместим один из них в process_item. Взглянув на консоль, мы увидим, что foo2 был уничтожен после того, как отработала функция process_item, поскольку мы передали ей право владения. Объект foo3 продолжит существовать до тех пор, пока не отработает функция main.
auto p1 (make_unique<Foo>("foo2"));
auto p2 (make_unique<Foo>("foo3"));
process_item(move(p1));
cout << "End of main()n";
}
7. Скомпилируем и запустим программу. Сначала мы увидим вызовы конструктора и деструктора для foo и bar. Они разрушаются после того, как программа покидает дополнительную область видимости. Обратите внимание: объекты разрушаются в порядке, обратном тому, в котором были созданы. Следующая строка конструктора принадлежит foo1 — мы создали этот объект во время вызова process_item. Он уничтожается сразу после вызова функции. Затем создали объекты foo2 и foo3. Первый из них уничтожается сразу после вызова process_item, где мы передали право владения. Другой элемент, foo3, разрушается после выполнения последней строки кода функции main.
$ ./unique_ptr
CTOR foo
CTOR bar
DTOR bar
DTOR foo
CTOR foo1
Processing foo1
DTOR foo1
CTOR foo2
CTOR foo3
Processing foo2
DTOR foo2
End of main()
DTOR foo3
Как это работает
Управлять объектами кучи с помощью std::unique_ptr очень легко. После того как мы инициализировали уникальный указатель так, чтобы он хранил указатель на некий объект, он не может быть случайно удален в какой-то ветке кода.
Если мы присвоим какой-то новый указатель уникальному указателю, то он сначала удалит старый объект, а только затем сохранит новый указатель. Для переменной уникального указателя x можно также вызвать x.reset() только затем, чтобы удалить объект, на который он указывает, не присваивая новый указатель. Еще одна эквивалентная альтернатива повторному присваиванию с помощью x = new_pointer — это x.reset(new_pointer).
Существует единственный способ освободить объект от управления unique_ptr без удаления самого объекта. Это делает функция release, но использовать ее в большинстве ситуаций не рекомендуется.
Поскольку указатели нужно проверять перед разыменованием, они переопределяют некоторые операторы так, чтобы те походили на необработанные указатели. Условия наподобие if (p) {...} и if (p != nullptr) {...} работают так же, как если бы мы проверяли необработанный указатель.
Разыменовать уникальный указатель можно с помощью функции get(), возвращающей необработанный указатель на объект, или непосредственно с применением operator*, что опять же делает их похожими на необработанные указатели.
Одна