Теперь, когда мы рассмотрели основы семантики перемещения, мы можем вернуться к теме умных указателей.
- Умные указатели
- Умный указатель std::unique_ptr
- Доступ к объекту, который хранит умный указатель
- Умный указатель std::unique_ptr и динамические массивы
- Функция std::make_unique()
- Безопасность использования исключений
- Возврат умного указателя std::unique_ptr из функции
- Передача умного указателя std::unique_ptr в функцию
- Умный указатель std::unique_ptr и классы
- Неправильное использование умного указателя std::unique_ptr
- Тест
Умные указатели
На предыдущих уроках мы говорили о том, как использование указателей может привести к ошибкам и утечкам памяти в некоторых ситуациях. Например, при досрочном завершении функции или при генерации исключения, когда указатель может не быть удален должным образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void someFunction() { Item *ptr = new Item; int a; std::cout << "Enter an integer: "; std::cin >> a; if (a == 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.
Рассмотрим простой пример использования std::unique_ptr:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { // Выделяем объект класса Item и передаем право собственности на него std::unique_ptr std::unique_ptr<Item> item(new Item); return 0; } // item выходит из области видимости здесь, соответственно, Item уничтожается также здесь |
Когда std::unique_ptr выходит из области видимости, он удаляет Item, которым владеет.
В отличие от std::auto_ptr, std::unique_ptr корректно реализовывает семантику перемещения:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { std::unique_ptr<Item> item1(new Item); // выделение Item std::unique_ptr<Item> item2; // присваивается значение nullptr std::cout << "item1 is " << (static_cast<bool>(item1) ? "not null\n" : "null\n"); std::cout << "item2 is " << (static_cast<bool>(item2) ? "not null\n" : "null\n"); // item2 = item1; // не скомпилируется: семантика копирования отключена item2 = std::move(item1); // item2 теперь владеет item1, а для item1 присваивается значение null std::cout << "Ownership transferred\n"; std::cout << "item1 is " << (static_cast<bool>(item1) ? "not null\n" : "null\n"); std::cout << "item2 is " << (static_cast<bool>(item2) ? "not null\n" : "null\n"); return 0; } // Item уничтожается здесь, когда item2 выходит из области видимости |
Результат выполнения программы:
Item acquired
item1 is not null
item2 is null
Ownership transferred
item1 is null
item2 is not null
Item destroyed
Поскольку std::unique_ptr разработан с учетом семантики перемещения, то семантика копирования по умолчанию отключена. Если вы хотите передать содержимое, управляемое std::unique_ptr, то вы должны использовать семантику перемещения. В программе, приведенной выше, мы передаем содержимое std::unique_ptr с помощью функции std::move() (которая конвертирует item1
в r-value, являющееся триггером для выполнения семантики перемещения вместо семантики копирования).
Доступ к объекту, который хранит умный указатель
Умный указатель std::unique_ptr имеет перегруженные операторы *
и ->
, которые используются для доступа к хранимым объектам. Оператор *
возвращает ссылку на управляемый ресурс, а оператор ->
возвращает указатель.
Умный указатель std::unique_ptr не всегда может управлять объектом: либо потому, что объект был создан пустым (с использованием конструктора по умолчанию, или в объект передан в качестве параметра nullptr), либо потому, что ресурс, которым он управлял, был перемещен в другой std::unique_ptr. Поэтому, прежде чем использовать какой-либо из этих операторов, вы должны проверить, действительно ли std::unique_ptr управляет ресурсом. К счастью, это легко сделать: std::unique_ptr имеет неявное преобразование в тип bool, возвращая true
, если std::unique_ptr владеет ресурсом. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; int main() { std::unique_ptr<Item> item(new Item); if (item) // используем неявное преобразование item в тип bool, чтобы убедиться, что item владеет Item-ом std::cout << *item; // выводим Item, которым владеет item return 0; } |
Результат:
Item acquired
I am an Item!
Item destroyed
В программе, приведенной выше, мы используем оператор *
для доступа к Item, которым владеет объект item
класса std::unique_ptr, который затем мы выводим с помощью std::cout.
Умный указатель std::unique_ptr и динамические массивы
В отличие от std::auto_ptr, std::unique_ptr достаточно умен, чтобы знать, когда использовать единичный оператор delete, а когда форму оператора delete для массива, поэтому std::unique_ptr можно использовать как с единичными объектами, так и с динамическими массивами.
Однако использование std::vector почти всегда является лучшим выбором, чем использование std::unique_ptr с динамическим массивом.
Правило: Используйте std::vector вместо использования умного указателя, который владеет динамическим массивом.
Функция std::make_unique()
В C++14 добавили новую функцию — std::make_unique(). Это шаблон функции, который создает объект типа шаблона и инициализирует его аргументами, переданными в функцию. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <iostream> #include <memory> // для std::unique_ptr и std::make_unique 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 с numerator = 7 и denominator = 9 std::unique_ptr<Fraction> f1 = std::make_unique<Fraction>(7, 9); std::cout << *f1 << '\n'; // Создаем объект с динамически выделенным массивом Fraction длиной 5. // Используем автоматическое определение типа данных с помощью ключевого слова auto auto f2 = std::make_unique<Fraction[]>(5); std::cout << f2[0] << '\n'; return 0; } |
Результат выполнения программы:
7/9
0/1
Использование функции std::make_unique() является необязательным, но рекомендуется вместо использования умного указателя std::unique_ptr. Дело в простоте. Кроме того, std::make_unique() решает проблему безопасности использования исключений, которая может возникнуть в результате неопределенного порядка обработки аргументов функции (так как язык С++ явно не указывает этот порядок).
Правило: Используйте функцию std::make_unique() вместо создания умного указателя std::unique_ptr и использования оператора new.
Безопасность использования исключений
Вот пример для тех, кто не понял, что это за проблема с безопасностью использования исключений, которая прозвучала выше:
1 |
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
не будет корректно удален, поскольку умный указатель, который должен выполнить его удаление, не успеет создаться. Это приведет к утечке памяти.
Функция std::make_unique() лишена этой проблемы, поскольку выделение объекта T
и создание std::unique_ptr происходят внутри функции std::make_unique(), где порядок обработки аргументов четко определен.
Возврат умного указателя std::unique_ptr из функции
Умный указатель std::unique_ptr можно возвращать из функции по значению:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::unique_ptr<Item> createItem() { return std::make_unique<Item>(); } int main() { std::unique_ptr<Item> ptr = createItem(); // Делаем что-либо return 0; } |
Здесь createItem() возвращает std::unique_ptr по значению. Если возвращаемое значение не присваивается какому-либо объекту, то оно выходит из области видимости, и Item удаляется. Если значение присваивается объекту (как показано в функции main()), то с помощью семантики перемещения Item перемещается из возвращаемого значения в нужный объект (в данном случае в ptr
). Это делает возврат ресурсов с помощью std::unique_ptr намного безопаснее, чем возврат «необработанных» указателей!
В общем, вы не должны возвращать std::unique_ptr по адресу (вообще) или по ссылке (если у вас нет на это веских причин).
Передача умного указателя std::unique_ptr в функцию
Если вы хотите, чтобы функция стала владельцем содержимого умного указателя, то передавать std::unique_ptr в функцию нужно по значению. Обратите внимание, поскольку семантика копирования отключена, то вам придется использовать std::move() для фактической передачи ресурса в функцию:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; void takeOwnership(std::unique_ptr<Item> item) { if (item) std::cout << *item; } // Item уничтожается здесь int main() { auto ptr = std::make_unique<Item>(); // takeOwnership(ptr); // это не скомпилируется. Мы должны использовать семантику перемещения takeOwnership(std::move(ptr)); // используем семантику перемещения std::cout << "Ending program\n"; return 0; } |
Результат выполнения программы:
Item acquired
I am an Item!
Item destroyed
Ending program
Обратите внимание, в данном случае право собственности на Item было передано в takeOwnership(), поэтому Item уничтожается в конце takeOwnership(), а не в конце main().
Однако в большинстве случаев вам не нужно будет, чтобы функция владела ресурсом. Хотя вы можете передать std::unique_ptr по ссылке (что позволит функции использовать объект без передачи ей права собственности на этот объект), вы должны делать это только тогда, когда caller может изменить передаваемый объект.
Вместо этого лучше передавать сам объект по адресу или по ссылке (в зависимости от того, является ли null допустимым аргументом). Это позволит функции оставаться в стороне от управления объектом. Чтобы получить необработанный указатель на объект из std::unique_ptr, мы можем использовать метод get():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; // Эта функция использует только Item, поэтому мы передаем указатель на Item, а не ссылку на весь std::unique_ptr<Item> void useItem(Item *item) { if (item) std::cout << *item; } int main() { auto ptr = std::make_unique<Item>(); useItem(ptr.get()); // примечание: Метод get() используется для получения указателя на Item std::cout << "Ending program\n"; return 0; } // Item уничтожается здесь |
Результат выполнения программы:
Item acquired
I am an Item!
Ending program
Item destroyed
Умный указатель std::unique_ptr и классы
Конечно, вы можете использовать std::unique_ptr в качестве члена композиции вашего класса. Таким образом, вам не нужно будет беспокоиться о том, удалит ли деструктор вашего класса ресурс std::unique_ptr, так как std::unique_ptr будет автоматически уничтожен при уничтожении объекта класса. Тем не менее, если объект вашего класса выделяется динамически, то сам ресурс std::unique_ptr подвергается риску неправильного удаления, и в таком случае даже умный указатель не поможет.
Неправильное использование умного указателя std::unique_ptr
Существует два способа неправильного использования std::unique_ptr, оба из которых легко избежать. Во-первых, не позволяйте нескольким классам «владеть» одним и тем же ресурсом. Например:
1 2 3 |
Item *item = new Item; std::unique_ptr<Item> item1(item); std::unique_ptr<Item> item2(item); |
Хотя это синтаксически допустимо, конечным результатом будет то, что и item1
, и item2
попытаются удалить Item, что приведет к неопределенному поведению/результатам.
Во-вторых, не удаляйте выделенный ресурс вручную из-под std::unique_ptr:
1 2 3 |
Item *item = new Item; std::unique_ptr<Item> item1(item); delete item; |
Если вы это сделаете, std::unique_ptr попытается удалить уже удаленный ресурс, что опять приведет к неопределенному поведению/результатам.
Обратите внимание, функция std::make_unique() предотвращает непреднамеренное возникновение обеих ситуаций, приведенных выше.
Тест
Задание №1
Если в вашем классе есть умный указатель в качестве члена вашего класса, то почему вы должны стараться избегать динамического выделения объектов этого класса?
Ответ №1
Умные указатели в качестве членов класса удаляют свой ресурс только в том случае, если объект класса выходит из области видимости. Если мы выделим объект класса динамически и не удалим его должным образом, то объект класса никогда не выйдет из области видимости, и умный указатель не сможет очистить ресурс, который он хранит.
Задание №2
Измените следующую программу, заменив обычный указатель на умный указатель std::unique_ptr, где это необходимо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#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* const ptr) { if (ptr) std::cout << *ptr; } int main() { Fraction *ptr = new Fraction(7, 9); printFraction(ptr); delete ptr; return 0; } |
Ответ №2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> #include <memory> // для std::unique_ptr 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, поэтому мы только его передаем. // Таким образом, мы можем не беспокоиться о том, какой умный указатель использует caller (если вообще использует) void printFraction(const Fraction* const ptr) { if (ptr) std::cout << *ptr; } int main() { auto ptr = std::make_unique<Fraction>(7, 9); printFraction(ptr.get()); return 0; } |
Разве item к моменту удаления не является nullptr? Ведь unique_ptr должен был украсть управление ресурсом у item и обнулить указатель.
Вроде бы осилил:
operator<< рекурсивно вызывается
измените класс и будет вам счастье 🙂
Здравствуйте!
Не очень понятно, с чего это следует избегать выделение памяти для объектов, содержащих std::unique_ptr, динамически. Да, конечно, надо их удалять, причём, например, с помощью того же unique_ptr.
Чем это хуже, чем выделение памяти для обычных объектов динамически?.. Что, теперь вообще не выделять память динамически? 🙂
А разве нельзя сделать так: void takeOwnership(std::unique_ptr<Item>&& item) ????
"Rvalue ссылка ведет себя точно так же, как и lvalue ссылка, за исключением того, что она может быть связана с временным объектом, тогда как lvalue связать с временным (не константным) объектом нельзя."
https://habr.com/ru/post/226229/
По идее ты просто передал указатель по ссылке и он ведёт себя как ссылка, в таком случае здесь нет семантики перемещения
при инициализации std::unique_ptr динамически выделенным массивом необходимо писать так :
Я же писал так :
Что приводило к удалению лишь первого элемента.
Я долго не мог понять почему так, и здесь примеров с массивами не видел.
И все же этот std::unique_ptr не так уж и "умен" раз ему надо говорить что это массив.
Есть инструкции языка, которые программист обязан соблюдать.
Чтобы умный указатель работал как положено с данным типом данных( массив) вы должны правильно передать этот тип данных в параметры щаблона — <Item[]> вместо <Item>
Это должно делаться так же как ставить точку с запятой после стейтментов.
Этот урок с одного захода осилить не получилось, я читала его два дня))
И всё равно есть чувство, что я чего-то недопонимаю и надо ещё пару раз перечитать…
В частности правило "не позволяйте нескольким классам «владеть» одним и тем же ресурсом" я бы переписала как "не позволяйте двум разным умным указателям владеть одним и тем же ресурсом".
И ещё какой-то полный хаос опять с этими ссылками и указателями. Здесь уже было несколько уроков, после которых я возвращалась к этим темам — и вновь задавалась вопросом, в каких случаях возвращать по ссылке, по указателю и в каких по значению, а передавать по чему?
В частности мы говорим, что в std::unique_ptr есть перегруженный оператор ->, который возвращает указатель. Дак нет же, есть ещё и метод get(), который возвращает указатель. Он делает то же самое? сомневаюсь) и так далее, и тому подобное. Мозг кипит от рассуждений вроде "как бы нам запихнуть в функцию указатель на умный указатель", а главное, зачем?..
Моё решение тестового задания. В нём я также отвечаю на вопросы, чем отличается умный указатель от его метода get() (ответ: ничем), и чем для умного указателя отличается get() от -> (ответ: первый — это сам умный указатель, а второй — это по сути указатель на метод класса, объект которого находится под управлением умного указателя)
Метод get() и operator->() возвращают одно и то же (указатель на объект хранимый в умном указателе). Просто если будет один оператор->, передавать в функцию копию указателя на объект будет не очень удобно. По этой же причине не очень удобно вызывать методы объекта класса, которым управляет умный указатель, через метод get().
Ценность этого комментария сравнима с ценностью всего урока 🙂
Добрый день!
Спасибо большое за уроки!
В задании 2 есть вариант передачи аргумента в функцию printFraction по константной ссылке на сам объект. Какой вариант использовать предпочтительнее?
Примерно такой ответ,я решил, что:
а)лучше, что бы printFraction вернул указатель на дробь, не void, так мы не теряем объект,а удалить его всегда успеем;
б)не пытайтесь передать дробь в printFraction по ссылке, т.к. объект std::unique_ptr может быть равен nullptr,и тогда полное фиаско;
Так лучше:
с++ и так сложен наличием у себя указателей, а тут вместо того чтобы наконец то от них избавиться их еще усложняют — адский ад, ничего не понятно
Так лучше,чем утечки иметь…
Половина уроков в итоге сводится к тому, что нам объясняют почему то, над чем мы часами сидели, пытаясь понять, использовать нельзя)
Ну не настолько всё категорично, как вы описываете. Но по ходу уроков действительно объясняются инструменты, использовать которые более рационально в определённых случаях, нежели применять молоток там, где нужен скальпель))
Ещё один нюанс заключается в том, что для того, чтобы объяснить, что такое скальпель, нужно сначала объяснить, что такое молоток. Начинаем с простых вещей и продвигаемся к более сложным.
По поводу типа auto. Не введет ли программиста в заблуждение использование auto. Мне вот, например, при виде auto ptr приходится весь код перечитывать, чтобы понять, что же там за этим авто кроется.
Добрый день!
Скажите, пожалуйста, а для каких целей надо вообще прибегать к созданию классов через оператор new? С динамическими массивами понятно, а классы выделять динамически зачем? Даже если сами классы используют внутри динамические массивы, это же не повод при их объявлении выделять память динамически.
Имеются в виду не классы а объекты этого класса. Фабрика например может создавать эти объекты.
Судя по количеству комментариев, до конца этих уроков дойдет далеко не каждый….
Вот здесь и проверяется желание — нужно ли это программирование или нет. Многие даже до 50-го урока не доходят, то к 200-ому уроку действительно самые отчаянные останутся 🙂
нам же лучше, меньше конкуренции 🙂
Останутся лишь те, кто действительно хочет получить знания!
Конечно, порог входа не столь низок, как это может показаться. Достаточно взглянуть на просмотры первых уроков и 100-ых/200-ых 🙂