что такое умные указатели с
Smart pointers для начинающих
Эта небольшая статья в первую очередь предназначена для начинающих C++ программистов, которые либо слышали об умных указателях, но боялись их применять, либо они устали следить за new-delete.
UPD: Статья писалась, когда C++11 еще не был так популярен.
Введение
Существует техника управления ресурсами посредством локальных объектов, называемая RAII. То есть, при получении какого-либо ресурса, его инициализируют в конструкторе, а, поработав с ним в функции, корректно освобождают в деструкторе. Ресурсом может быть что угодно, к примеру файл, сетевое соединение, а в нашем случае блок памяти. Вот простейший пример:
Это удобно: по выходу из функции нам не нужно заботиться об освобождении буфера, так как для объекта screen вызовется деструктор, который в свою очередь освободит инкапсулированный в себе массив пикселей. Конечно, можно написать и так:
В принципе, никакой разницы, но представим себе такой код:
Придется в каждой ветке выхода из функции писать delete [], либо вызывать какие-либо дополнительные функции деинициализации. А если выделений памяти много, либо они происходят в разных частях функции? Уследить за всем этим будет все сложнее и сложнее. Подобная ситуация возникает, если мы в середине функции бросаем исключение: гарантируется, что объекты на стеке будут уничтожены, но с кучей проблема остается открытой.
Ок, будем использовать RAII, в конструкторах инициализировать память, в деструкторе освобождать. И пусть поля нашего класса будут указателями на участки динамической памяти:
Простейший smart pointer
boost::scoped_ptr
Он находится в библиотеке буст.
Реализация простая и понятная, практически идентичная нашей, за несколькими исключениями, одно из них: этот пойнтер не может быть скопирован (то есть у него приватный конструктор копирования и оператор присваивания). Поясню на примере:
Оно и понятно, если бы было разрешено присваивание, то и p1 и p2 будут указывать на одну и ту же область памяти. А по выходу из функции оба удалятся. Что будет? Никто не знает. Соответственно, этот пойнтер нельзя передавать и в функции.
Тогда зачем он нужен? Советую применять его как указатель-обертка для каких-либо данных, которые выделяются динамически в начале функции и удаляются в конце, чтобы избавить себя от головной боли по поводу корректной очистки ресурсов.
Подробное описание здесь.
std::auto_ptr
Чуть-чуть улучшенный вариант предыдущего, к тому же он есть в стандартной библиотеке (хотя в C++11 вроде как deprecated). У него есть оператор присваивания и конструктор-копировщик, но работают они несколько необычно.
Поясняю:
Теперь при присваивании в p2 будет лежать указатель на MyObject (который мы создавали для p1), а в p1 не будет ничего. То есть p1 теперь обнулен. Это так называемая семантика перемещения. Кстати, оператор копирования поступает таким же образом.
Зачем это нужно? Ну например у вас есть функция, которая должна создавать какой-то объект:
Это означает, что функция создает новый объект типа MyObject и отдает его вам в распоряжение. Понятней станет, если эта функция сама является членом класса (допустим Factory): вы уверены, что этот класс (Factory) не хранит в себе еще один указатель на новый объект. Объект ваш и указатель на него один.
В силу такой необычной семантики auto_ptr нельзя использовать в контейнерах STL. Но у нас есть shared_ptr.
std::shared_ptr (С++11)
Умный указатель с подсчетом ссылок. Что это значит. Это значит, что где-то есть некая переменная, которая хранит количество указателей, которые ссылаются на объект. Если эта переменная становится равной нулю, то объект уничтожается. Счетчик инкрементируется при каждом вызове либо оператора копирования либо оператора присваивания. Так же у shared_ptr есть оператор приведения к bool, что в итоге дает нам привычный синтаксис указателей, не заботясь об освобождении памяти.
Теперь и p2 и p1 указывают на один объект, а счетчик ссылок равен 2, По выходу из скоупа счетчик обнуляется, и объект уничтожается. Мы можем передавать этот указатель в функцию:
Заметьте, если вы передаете указатель по ссылке, то счетчик не будет увеличен. Вы должны быть уверены, что объект MyObject будет жив, пока будет выполняться функция test.
Итак, smart pointers это хорошо, но есть и минусы.
Во-первых это небольшой оверхед, но я думаю у вас найдется несколько тактов процессора ради такого удобства.
Во-вторых это boiler-plate, например
Это частично можно решить при помощи дефайнов, допустим:
Либо при помощи typedef.
В-третьих, существует проблема циклических ссылок. Рассматривать ее здесь не буду, чтобы не увеличивать статью. Так же остались нерассмотренными boost::weak_ptr, boost::intrusive_ptr и указатели для массивов.
Кстати, smart pointers достаточно хорошо описаны у Джеффа Элджера в книге «С++ for real programmers».
Записки программиста
Шпаргалка по использованию умных указателей в C++
Благодаря наличию исключений, язык C++ позволяет разделить основную логику приложения и обработку ошибок, не мешая их в одну кучу. Что есть очень хорошо. Однако теперь по коду нельзя с уверенностью сказать, где может быть прервано его исполнение. Отсюда возникает опасность утечки ресурсов. Проблема эта решается при помощи деструкторов и идиомы RAII. Впрочем, придерживаться этой идиомы становится проблематично при использовании указателей. Особенно при использовании их не как членов класса, а просто как переменных в методах. На наше с вами счастье, в стандартной библиотеке языка есть умные указатели (smart pointers), придуманные именно для этого случая. Поскольку на C++ я пишу не регулярно, то иногда забываю некоторые нюансы использования умных указателей, в связи с чем решил вот набросать небольшую шпаргалку.
Важно! В старых книжках и статьях можно встретить упоминание auto_ptr. Этот тип умных указателей появился в C++, когда в языке еще не было move semantics. Из-за этого использование auto_ptr порой может приводить к трудным в обнаружении ошибкам. В стандарте C++17 auto_ptr был удален. Другими словами, все, что вы должны знать об auto_ptr — это то, что его не должно быть в современном коде. Вместо него всегда используйте unique_ptr.
unique_ptr
Шаблонный класс unique_ptr представляет собой уникальный указатель на объект. Указатель нельзя копировать, но можно передавать владение им с помощью std::move. При уничтожении указателя автоматически вызывается деструктор объекта, на который он указывает.
Создается unique_ptr так:
… но обычно используют шаблон make_unique, так короче:
Как уже отмечалось, unique_ptr запрещено копировать:
Однако владение им можно передать при помощи std::move, например:
Плюс к этому, мы всегда можем получать из unique_ptr обычный указатель на объект:
… хотя это и является code smell. Кроме того, ничто не мешает создавать ссылки (reference) на unique_ptr:
То есть, в этом случае мы как бы не отнимаем владение объектом, а ненадолго одалживаем его, обращаясь к нему через все тот же умный указатель.
Интересно, что unique_ptr позволяет указать функцию, которую он будет вызывать вместо деструктора, так называемый custom deleter. Это позволяет использовать unique_ptr с ресурсами, возвращаемых из библиотек для языка C, и даже реализовать аналог defer из языка Go:
#include
#include
#include
#include
template typename T >
using auto_cleanup = std :: unique_ptr T,std :: function void ( T * ) >> ;
#define _DEFER_CAT_(a,b) a##b
#define _DEFER_NAME_(a,b) _DEFER_CAT_(a,b)
#define defer(. ) \
auto _DEFER_NAME_(_defer_,__LINE__) = \
auto_cleanup (dummy, [&](char*) < __VA_ARGS__; >);
defer ( std :: cout «Bye #1» std :: endl ) ;
defer ( std :: cout «Bye #2» std :: endl ) ;
Заметьте, что в макросе defer нам пришлось передать в unique_ptr фиктивный указатель. Если бы мы передали nullptr, custom deleter не был бы вызван.
shared_ptr и weak_ptr
Класс shared_ptr является указатем на объект, которым владеет сразу несколько объектов. Указатель можно как перемещать, так и копировать. Число существующих указателей отслеживается при помощи счетчика ссылок. Когда счетчик ссылок обнуляется, вызывается деструктор объекта. Сам по себе shared_ptr является thread-safe, но он не делает магическим образом thread-safe объект, на который ссылается. То есть, если доступ к объекту может осуществляться из нескольких потоков, вы должны не забыть предусмотреть в нем мьютексы или что-то такое.
Для создания shared_ptr обычно используется шаблон make_shared:
В остальном работа с ним мало отличается от работы с unique_ptr, за тем исключением, что shared_ptr можно смело копировать.
Интересные грабли при использовании shared_ptr заключается в том, что с его помощью можно создать циклические ссылки. Например, есть два объекта. Первый ссылается при помощи shared_ptr на второй, а второй — на первый. Даже если ни на один из объектов нет других ссылок, счетчики ссылок никогда не обнулятся, и объекты никогда не будут уничтожены.
class SomeClass <
public :
void sayHello ( ) <
std :: cout «Hello!» std :: endl ;
>
SomeClass ( ) <
std :: cout «
int main ( ) <
std :: weak_ptr SomeClass > wptr ;
<
auto ptr = std :: make_shared SomeClass > ( ) ;
wptr = ptr ;
SomeClass
lock() failed
Можно думать о weak_ptr как об указателе, позволяющим получить временное владение объектом. Само собой разумеется, если все постоянные указатели на объект перестанут существовать, и останутся только временные, полученные при помощи метода lock() класса weak_ptr, объект продолжит свое существование. Он будет уничтожен только тогда, когда на объект не останется вообще никаких указателей.
Умные указатели и наследование
Вопрос, о котором часто забывают — это кастование умных указателей вверх и вниз по иерархии классов. Для shared_ptr в стандартной библиотеке есть шаблоны static_pointer_cast, dynamic_pointer_cast и другие. Для unique_ptr таких же шаблонов почему-то не занесли, но их нетрудно найти на StackOverflow.
// https://stackoverflow.com/a/21174979/1565238
template typename Derived, typename Base, typename Del >
std :: unique_ptr Derived, Del >
static_unique_ptr_cast ( std :: unique_ptr Base, Del > && p )
<
auto d = static_cast Derived * > ( p. release ( ) ) ;
return std :: unique_ptr Derived, Del > ( d,
std :: move ( p. get_deleter ( ) ) ) ;
>
template typename Derived, typename Base, typename Del >
std :: unique_ptr Derived, Del >
dynamic_unique_ptr_cast ( std :: unique_ptr Base, Del > && p )
<
if ( Derived * result = dynamic_cast Derived * > ( p. get ( ) ) ) <
p. release ( ) ;
return std :: unique_ptr Derived, Del > ( result,
std :: move ( p. get_deleter ( ) ) ) ;
>
return std :: unique_ptr Derived, Del > ( nullptr, p. get_deleter ( ) ) ;
>
class Base <
public :
Base ( int num ) : num ( num ) < >;
virtual void sayHello ( ) <
std :: cout «I’m Base #» num std :: endl ;
>
Base #» num std :: endl ;
>
protected :
int num ;
> ;
class Derived : public Base <
public :
Derived ( int num ) : Base ( num )
virtual void sayHello ( ) <
std :: cout «I’m Derived #» num std :: endl ;
>
Derived ( ) <
std :: cout «
Derived #» num std :: endl ;
>
> ;
void testUnique ( ) <
std :: cout «=== testUnique begin ===» std :: endl ;
std :: cout «=== testUnique end ===» std :: endl ;
>
void testShared ( ) <
std :: cout «=== testShared begin ===» std :: endl ;
std :: cout «=== testShared end ===» std :: endl ;
>
int main ( ) <
testUnique ( ) ;
testShared ( ) ;
>
Как видите, все оказалось не так уж и сложно.
Заключение
По моим представлениям, приведенной шпаргалки должно хватать в
Интеллектуальные указатели (современный C++)
В современном программировании на C++ Стандартная библиотека содержит смарт-указатели, которые позволяют гарантировать, что программы свободны от памяти, утечки ресурсов и являются надежными.
Использование интеллектуальных указателей
В большинстве случаев при инициализации необработанного указателя или дескриптора ресурса для указания на фактический ресурс следует сразу же передать указатель в интеллектуальный указатель. В современном C++ необработанные указатели используются только в небольших блоках кода с ограниченной областью, циклах или вспомогательных функциях, когда важна производительность и вероятность проблем с владением низкая.
В следующем примере сравниваются объявления необработанного и интеллектуального указателей.
Как показано в примере, интеллектуальный указатель — это шаблон класса, который объявляется в стеке и инициализируется с помощью необработанного указателя, указывающего на размещенный в куче объект. После инициализации интеллектуальный указатель становится владельцем необработанного указателя. Это означает, что интеллектуальный указатель отвечает за удаление памяти, заданной необработанным указателем. Деструктор интеллектуального указателя содержит вызов для удаления, и поскольку интеллектуальный указатель объявлен в стеке, его деструктор вызывается, как только интеллектуальный указатель оказывается вне области, даже если исключение создается где-либо в другой части стека.
Этот интеллектуальный указатель C++ напоминает создание объектов в таких языках, как C#: вы создаете объект, а система удаляет его в правильный момент. Отличие заключается в том, что отсутствует отдельный сборщик мусора, работающий в фоновом режиме; память управляется через стандартные правила области C++, чтобы среда выполнения функционировала быстрее и эффективнее.
Всегда создавайте интеллектуальные указатели в отдельной строке кода; ни в коем случае не делайте это в списке параметров, чтобы не произошла небольшая утечка ресурсов, связанная с определенными правилами выделения памяти спискам параметров.
В следующем примере показано, как unique_ptr тип интеллектуального указателя из стандартной библиотеки C++ можно использовать для инкапсуляции указателя на большой объект.
В этом примере показаны следующие важные шаги, необходимые для использования интеллектуальных указателей.
Объявите интеллектуальный указатель как автоматическую (локальную) переменную. (Не используйте new malloc выражение или для самого интеллектуального указателя.)
В параметре типа укажите тип, на который указывает инкапсулированный указатель.
Передайте необработанный указатель на new объект-ED в конструкторе интеллектуального указателя. (Некоторые служебные функции или конструкторы интеллектуальных указателей делают это автоматически.)
Интеллектуальный указатель удаляет объект автоматически.
Интеллектуальные указатели разработаны для обеспечения максимальной эффективности в отношении памяти и производительности. Например, единственный элемент данных в unique_ptr — это инкапсулированный указатель. Это означает, что размер unique_ptr точно такой же, как и у указателя — 4 или 8 байтов. Доступ к инкапсулированному указателю с помощью перегрузки смарт-указателя * и- > Operators значительно медленнее, чем доступ к необработанным указателям напрямую.
Интеллектуальные указатели имеют собственные функции-члены, доступ к которым осуществляется с помощью нотации «точка». Например, некоторые интеллектуальные указатели стандартной библиотеки C++ имеют функцию-член reset, которая освобождает владение указателем. Это полезно, когда нужно освободить память, принадлежащую интеллектуальному указателю, не дожидаясь, пока интеллектуальный указатель окажется вне области, как показано в следующем примере.
Смарт-указатели обычно предоставляют способ прямого доступа к необработанному указателю. Интеллектуальные указатели стандартной библиотеки C++ имеют get функцию-член для этой цели и CComPtr имеют открытый p член класса. Предоставляя прямой доступ к базовому указателю, можно использовать интеллектуальный указатель для управления памятью в своем коде и по-прежнему передавать необработанный указатель коду, который не поддерживает интеллектуальные указатели.
Виды смарт-указателей
В следующем разделе приведены различные виды интеллектуальных указателей, доступные в среде программирования Windows, и приводится описание их использования.
Интеллектуальные указатели стандартной библиотеки C++
Используйте эти интеллектуальные указатели как основной вариант для инкапсуляции указателей на простые старые объекты C++ (POCO).
интеллектуальные указатели для COM-объектов (классическое программирование Windows)
Класс Ккомхеапптр
Интеллектуальный указатель на объекты, которые используют CoTaskMemFree для освобождения памяти.
Класс Ккомгитптр
Интеллектуальный указатель для интерфейсов, получаемых из глобальной таблицы интерфейсов (GIT).
Интеллектуальные указатели ATL для объектов POCO
Помимо смарт-указателей для COM-объектов, ATL также определяет смарт-указатели и коллекции смарт-указателей для простых старых объектов C++ (POCO). в классическом Windows программировании эти типы являются полезными альтернативами для коллекций стандартной библиотеки c++, особенно если переносимость кода не требуется или если не требуется смешивать модели программирования стандартной библиотеки c++ и ATL.
Класс Чеапптр
Интеллектуальный указатель для объектов, которые выделены с помощью функции malloc C.
🛠 Умные указатели в C++
wcobalt
Как C++ управляет памятью?
Когда мы говорим про управление памятью в C++, мы неизменно обращаемся к термину storage duration (длительность хранения). Storage duration – это свойство объекта, которое описывает, когда тот попадает в память и когда её освобождает.
В C++ существует четыре вида [1] storage duration:
Можно сказать, что в случае с автоматической storage duration память освобождается автоматически, а в случае с динамической – вручную. Почему же тогда не использовать всегда автоматическую память?
Чтобы обойти эти ограничения, необходимо использовать динамическую память про использование которой мы и будем сегодня говорить.
Что такое умные указатели и зачем они нужны?
Используем динамическую память, отлично. Теперь объекты могут покидать область видимости, где были созданы, и иметь определяемый во время выполнения размер – жизнь стала налаживаться и жаловаться как будто не на что.
Предлагаем взглянуть на следующий фрагмент кода:
На первый взгляд, здесь всё хорошо, но есть нюансы:
При использование простых указателей (также известных как raw pointers) невозможно без дополнительных комментариев или дополнительного изучения кода определить, какой указатель объектом владеет, а какой – только использует. Взгляните на следующую декларацию:
Главная проблема здесь, что тому, кто будет вызывать функцию, совершенно неясно, должен он вызвать delete для возвращаемого указателя или за это ответственен код где-то в другой части программы. Иначе говоря, здесь не видно, является указатель владеющим или использующим.
Все вышеназванные проблемы изящно решаются умными указателями. Умные указатели в C++ – это не что-то магическое, встроенное в синтаксис языка, а не более чем набор классов из стандартной библиотеки. Разберёмся с ними один за одним.
std::unique_ptr
Первым умным указателем, с которым мы познакомимся, будет std::unique_ptr [3]. Он ссылается на объект в динамической памяти и при выходе из области видимости уничтожает хранимый объект. Взглянем на пример кода ниже:
Когда std::unique_ptr выходит из области видимости, утечки памяти не происходит, потому что в своем деструкторе умный указатель вызывает delete для объекта на который ссылается, высвобождая тем самым память.
От проблем с внезапными исключениями использующих умные указатели (в частности std::unique_ptr ) программистов защищает развёртывание стека (stack-unwinding [4]).
Подробное рассмотрение этого механизма С++ выходит за рамки статьи, но главное, что нужно знать о нём – если на стеке был создан объект, а после этого было выброшено исключение, C++ гарантированно вызовет деструктор для этого объекта. Это значит, что если мы обновим код в листинге 3 так, чтобы он использовал умные указатели, то избавимся от всех трёх вышеназванных проблем:
Здесь возможен следующий порядок вычисления аргументов:
Если при вызове new B() произойдет исключение, занятая при вызове new A() память не освободится, потому что умный указатель для этого объекта ещё не был создан, а delete никто вызывать и не собирался. Использование std::make_unique решает подобные проблемы.
std::unique_ptr используется тогда, когда объект должен иметь только одного владельца, однако мы можем передать право на владение кому-то другому. Чтобы это сделать, необходимо использовать std::move [6]. Рассмотрим код:
Когда в коде используются простые указатели и умные, сразу становится понятно, где указатель владеет объектом, а где – только использует.
std::unique_ptr – это умный указатель, о котором вы должны подумать в первую очередь, когда решите разместить что-нибудь в динамической памяти. Это ваш умный указатель по умолчанию.
std::shared_ptr и std::weak_ptr
std::unique_ptr и правда хорош, но он не поможет в ситуации, когда мы хотим, чтобы несколько объектов работали с одним общим ресурсом и чтобы в момент, когда все эти объекты были выгружены из памяти, за ненадобностью автоматически выгрузился бы и ресурс.
Вообще говоря, std::weak_ptr необходимо использовать всегда, когда надо ссылаться на управляемый std::shared_ptr объект, но не защищать его от уничтожения.
Выводы
Каждый программист на C++ должен уметь использовать умные указатели. Умные указатели – это ваш способ управления динамической памятью по умолчанию. std::unique_ptr – это ваш умный указатель по умолчанию. Использование умных указателей не противоречит использованию простых указателей, в случае, если последние используют объекты, а не владеют ими. std::auto_ptr – зло.
Урок №189. Умные указатели и Семантика перемещения
Обновл. 15 Сен 2021 |
На этом уроке мы рассмотрим, что такое умные указатели и семантика перемещения в языке С++.
Проблема
Умные указатели
Одна из лучших особенностей классов — это деструкторы, которые автоматически выполняются при выходе объекта класса из области видимости. При выделении памяти в конструкторе класса, вы можете быть уверены, что эта память будет освобождена в деструкторе при уничтожении объекта класса (независимо от того, выйдет ли он из области видимости, будет ли явно удален и т.д.). Это лежит в основе парадигмы программирования RAII.
Так что же, выходом является использование класса для управления указателями и выполнения соответствующей очистки памяти? Да, именно так!
Например, рассмотрим класс, единственными задачами которого является хранение и «управление» переданным ему указателем, а затем корректное освобождение памяти при выходе объекта класса из области видимости. До того момента, пока объекты этого класса создаются как локальные переменные, мы можем гарантировать, что, как только они выйдут из области видимости (независимо от того, когда или как), переданный указатель будет уничтожен.
Вот первый набросок:
Результат выполнения программы:
Item acquired
Item destroyed
Рассмотрим детально, как работают эти программа и класс. Сначала мы динамически выделяем объект класса Item и передаем его в качестве параметра нашему шаблону класса Auto_ptr1. С этого момента объект item класса Auto_ptr1 владеет выделенным объектом класса Item (Auto_ptr1 имеет композиционную связь с m_ptr ). Поскольку item объявлен в качестве локальной переменной и имеет область видимости блока, он выйдет из области видимости после завершения выполнения блока, в котором находится, и будет уничтожен. А поскольку это объект класса, то при его уничтожении будет вызван деструктор Auto_ptr1. Этот деструктор и обеспечит удаление указателя Item, который он хранит!
До тех пор, пока объект класса Auto_ptr1 определен как локальная переменная (с автоматической продолжительностью жизни, отсюда и часть «Auto» в имени класса), Item гарантированно будет уничтожен в конце блока, в котором он объявлен, независимо от того, как этот блок (функция main()) завершит свое выполнение (досрочно или нет).
Такой класс называется умным указателем. Умный указатель — это класс, предназначенный для управления динамически выделенной памятью и обеспечения освобождения (удаления) выделенной памяти при выходе объекта этого класса из области видимости. Соответственно, встроенные (обычные) указатели иногда еще называют «глупыми указателями», так как они не могут выполнять после себя очистку памяти.
Теперь вернемся к нашему примеру с myFunction() и покажем, как использование класса умного указателя сможет решить нашу проблему:
Если пользователь введет ненулевое целое число, то результат выполнения программы:
Item acquired
Enter an integer: 7
Hi!
Item destroyed
Если же пользователь введет ноль, то функция myFunction() завершит свое выполнение досрочно, и мы увидим:
Item acquired
Enter an integer: 0
Item destroyed
Обратите внимание, даже в случае, когда пользователь введет ноль, и функция завершит свое выполнение досрочно, Item по-прежнему будет корректно удален.
Поскольку переменная ptr является локальной переменной, то она уничтожается при завершении выполнения функции (независимо от того, как это будет сделано: досрочно или нет). И поскольку деструктор Auto_ptr1 выполняет очистку Item, то мы можем быть уверены, что Item будет корректно удален.
Критический недостаток
Класс Auto_ptr1, приведенный выше, имеет критическую ошибку, которая скрывается за некоторым автоматически генерируемым кодом. Прежде чем продолжить, посмотрите, сможете ли вы определить, что это за ошибка.
Подсказка: Подумайте, какие части класса генерируются автоматически, если вы их не предоставляете самостоятельно.
Хорошо, время истекло.
Мы не будем сейчас вам это говорить, мы сейчас вам это покажем:
Результат выполнения программы:
Item acquired
Item destroyed
Item destroyed
Вы получите ту же проблему, используя следующую функцию:
В этой программе item1 передается по значению в параметр item функции passByValue(), что приведет к дублированию указателя Item. Мы вновь получим «Бум!».
Так быть не должно. Что мы можем сделать?
Мы можем явно определить и удалить конструктор копирования с оператором присваивания, тем самым предотвращая выполнение любого копирования. Это также предотвратит передачу по значению.
Но как нам тогда вернуть Auto_ptr1 из функции обратно в caller?
Другой вариант — переопределить конструктор копирования и оператор присваивания для выполнения глубокого копирования. Таким образом, мы, по крайней мере, гарантированно избежим дублирования указателей (которые будут указывать на один и тот же объект). Но глубокое копирование может быть затратной операцией (а также нежелательной или даже невозможной), и мы не хотим делать ненужные копии объектов просто для того, чтобы вернуть Auto_ptr1 из функции. Кроме того, присваивание или инициализация глупого указателя не копирует объект, на который указывает, так почему же мы ожидаем, что умные указатели будут вести себя по-другому?
Семантика перемещения
А что, если бы наш конструктор копирования и оператор присваивания не копировали указатель (семантика копирования), а передавали владение указателем из источника в объект назначения? Это основная идея семантики перемещения. Семантика перемещения означает, что класс, вместо копирования, передает право собственности на объект.
Давайте обновим наш класс Auto_ptr1 с использованием семантики перемещения:
Результат выполнения программы:
Item acquired
item1 is not null
item2 is null
Ownership transferred
item1 is null
item2 is not null
Item destroyed
std::auto_ptr и почему его лучше не использовать
Теперь самое время поговорить о std::auto_ptr. std::auto_ptr, представленный в C++98, был первой попыткой в языке C++ сделать стандартизированный умный указатель. В std::auto_ptr решили реализовать семантику перемещения точно так же, как это сделано в классе Auto_ptr2.
Однако, std::auto_ptr (как и наш класс Auto_ptr2) имеет ряд проблем, которые делают его использование опасным.
Во-первых, поскольку std::auto_ptr реализовывает семантику перемещения через конструктор копирования и оператор присваивания, то передача std::auto_ptr в функцию по значению приведет к тому, что ваш Item будет перемещен в параметр функции и, следовательно, будет уничтожен в конце функции, когда параметры этой функции выйдут из области видимости (в нашем классе Auto_ptr2 передача выполняется по ссылке). Затем, когда вы попытаетесь получить доступ к аргументу std::auto_ptr из caller-а (не осознавая, что он был передан и удален), вы внезапно выполните разыменование нулевого указателя. Бум!
Во-вторых, std::auto_ptr всегда удаляет свое содержимое, используя оператор delete, который не работает с массивами. Это означает, что std::auto_ptr не будет правильно работать с динамическими массивами, поскольку использует неправильный тип удаления. Хуже того, std::auto_ptr не помешает вам передать ему динамический массив, который затем будет неправильно обработан, что приведет к утечке памяти.
Наконец, std::auto_ptr не очень хорошо работает со многими другими классами из Стандартной библиотеки С++ (особенно с контейнерными классами и классами алгоритмов). Это происходит из-за того, что классы Стандартной библиотеки С++ предполагают, что, когда они копируют элемент, они фактически выполняют копирование, а не перемещение.
Из-за вышеупомянутых недостатков в C++11 перестали использовать std::auto_ptr, а в C++17 планировали удалить его из Стандартной библиотеки С++.
Правило: std::auto_ptr устарел и не должен использоваться. Используйте вместо него std::unique_ptr или std::shared_ptr.
Что дальше?
Основная проблема с std::auto_ptr заключается в том, что до C++11 в языке C++ просто не было механизма, позволяющего отличить «семантику копирования» от «семантики перемещения». Переопределение семантики копирования для реализации семантики перемещения привело к неопределенным результатам и непреднамеренным ошибкам. Например, вы можете написать item1 = item2 и вообще не знать, изменится ли item2 или нет!
По этой причине в C++11 понятие «перемещение» было определено формально, в следствии чего в язык С++ было добавлено «семантику перемещения», чтобы должным образом отличать копирование от перемещения. Теперь, когда вы понимаете, чем семантика перемещения может быть полезной, мы рассмотрим её детально на следующих уроках.
В C++11 std::auto_ptr был заменен кучей других типов умных указателей:
Мы также рассмотрим два самых популярных из них: std::unique_ptr (который является прямой заменой std::auto_ptr) и std::shared_ptr.
Поделиться в социальных сетях:
Глава №14. Итоговый тест
Комментариев: 8
Наверное потому, что этот нюанс подробно рассматривался в предыдущих уроках?
Интересно, а можно ли помимо самого указателя, в умном указателе держать счетчик (количество держателей указателя), который бы при создании, присваивании увеличивалсяч на 1, а при работе деструктора уменьшался на 1. В, общем так, как реализовывается подсчет ссылок в COM объектах. И когда доходит до 0 — должно происходить освобождение памяти.