Умные указатели

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

А минимум заключается в следующем:
Отличия умного указателя (smart pointer) от стандартного указателя (raw pointer) в том что:

  • Умный указатель по выходу из области видимости – сам уничтожает объект, на который он указывает.

Пожалуй, это главное свойство, присущее всем разновидностям умных указателей.

  • Совместное владение.

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

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

С умными же указателями, поддерживающими совместное владение - все тривиально - в главной ф-ии достаточно создать динамический объект и передать его во владение умному указателю, а далее не нужно заботиться об уничтожении объекта – он уничтожится именно тогда, когда перестанет быть нужным – т.е. когда последний из умных указателей, владеющий объектом, уйдет из области видимости. Пример.

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

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

Указуемый объект один, а смартпоинтеров много – напрашивается вывод что количество «ссылок» так же должно быть одним, т.е. общим для всех смартпоинтеров. И это количество невозможно хранить «локально», в объекте умного указателя. Иначе говоря, когда существует указуемый объект, тогда в памяти так же существует другой динамический объект, в котором хранится количество ссылок на указуемый объект.

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

Так же в служебной информации необходимо хранить информацию о способе удаления указуемого объекта. Дело в том что с обычными указателями программист сам контролирует механику удаления – если это указатель на единичный объект, программист выберет операцию delete для удаления, в случае если это массив (непрерывный ряд единичных указателей) выберет операцию delete []. К сожалению smartpointer не имеет возможности узнать какого рода в нём объект – единичный, или же «последовательный», поэтому необходимо сообщать методы удаления.

Немного истории:

Указанные выше свойства – характерны для умного указателя std::shared_ptr. Который появился начиная с Си++11. До этого, в «классическом» Си++ (классика конечно же условно, и обусловлена лишь той эпохой, что большинство книг и написанного кода, сейчас соответствуют стандарту, предшествующему 11-му ) не было поддержки (стандартизованной реализации) умных указателей. Очень коротко говоря, для поддержки, скажем так, «философии» умных указателей – нужна операция исключающего владения. Например, умный указатель, описанный выше имеет тип совместного владения. При исключающем – только один экземпляр умного указателя владеет объектом.

До 11го стандарта Cи++ имел единственный умный указатель std::auto_ptr . Этот указатель - присваивал своему внутреннему полю значение элемента от другого auto_ptr, к которому его присваивали, а поле прежнего auto_ptr переставало указывать на элемет (=0). Это необходимо для организации исключительного владения (которое нужно чтобы избежать удаление уже удаленного объекта). Это не давало возможным использовать такие указатели в контейнерах (подробнее здесь). А нужен такой умный указатель для "фабрик" - он гарантировал то, что можно создать объект в "фабрике" (т.е. некоторой ф-ии) и вернуть из неё наружу созданный объект, через auto_ptr, при этом после передачи из "фабрики" не нужно следить за очищением выделенной памяти - она очистится автоматически. Хорошо полезность auto_ptr описана здесь.

В Си++11 появилась семантика переноса и конструкторы переноса – это позволило более эффективно реализовать принципы auto_ptr в unique_ptr (а auto_ptr устарел), так же добавили и другие указатели, в том числе и shared_pointer. Почему же не добавили shared_pointer в прежний Си++, ведь видно что мы его можем просто реализовать. Но это лишь учебная реализация, которая показывает философию умного указателя, вполне работоспособная в однопоточных программах. В многопоточном приложении может случится некоторая неопределенность над этим учебным указателем (например одновременно в двух потоках происходит декремент и инкремент счетчика ссылок). В Си++11 ввели поддержку «атомарных операций», и именно на основе таких операций работают операции над служебными данными – это гарантирует правильность операций в многопоточных программах, использующих один и тот же объект общего владения. В отличие от счетчика, работа с указуемым объектом неатомарна, и обеспечение однозначности операций в многопточности отдано на усмотрение пользователю умного указателя. Весьма логичное решение - полное эмулироание обычных указателей, в том числе и в вопросах работы с мьютексами.

Кстати говоря, в учебном или, скажем так, собеседовательном умном указателе, как и в shared_ptr есть особенность, которую нужно знать. При создании такого указателя - лучше всего в его конструкторе или операции присваивания, использовать безымянную величину, это может избавить от ошибок такого рода:

  1. int * raw_pointer = new int (13);
  2. std::shared_ptr<int> smart_pointer1 ( raw_pointer );
  3. std::shared_ptr<int> smart_pointer2 ( raw_pointer );

При уничтожении одного из умных указателей, второй не имеет возможности, внутри себя, узнать указывает ли его внутреннее поля на существующий объект или же на уже освобожденную память, и будет неопределенное поведение, valgrind сругается на "Invalid free() / delete / delete[] / realloc()" - лишнюю операцию очистки.

Правильно будет так:

  1. int * raw_pointer = new int (13);
  2. std::shared_ptr<int> smart_pointer1 ( raw_pointer );
  3. std::shared_ptr<int> smart_pointer2 ( smart_pointer1 );

А ещё лучше так:

  1. std::shared_ptr<int> smart_pointer1 ( new int (13) );
  2. std::shared_ptr<int> smart_pointer2 ( smart_pointer1 );

Вот приблизительно такого рода код, наверное, и хотели бы видеть на собеседовании:

  1. #include <iostream>
  2. using std::cout;
  3. using std::cerr;
  4. using std::string;
  5.  
  6. // Обработчики удаления
  7. template <class T> void single_delete (T * t) { cout << "remove single pointer\n"; delete t; }
  8. template <class T> void seq_delete (T * t) { cout << "remove sequence pointer\n"; delete [ ] t; }
  9.  
  10. // Класс служебной информации о блоке
  11. template <typename T> struct ControlBlock
  12. {
  13. unsigned int count; // Количество ссылок на элемент
  14. void (*deleterPtr_)(T*); // Указатель на функцию удаления
  15. ControlBlock ( int cnt = 0 , void (*deleterPtr)(T*) = nullptr ):
  16. count(cnt), deleterPtr_(deleterPtr) { }
  17. };
  18.  
  19. // Класс умного указателя ( упрощенное подобие std::shared_ptr ( C++11 ) )
  20. // Наиболее удачным решением, по хранению ссылок на объект в памяти
  21. // является использование внешнего класса, причем динамического (время жизни
  22. // которого будет совпадать с временем жизни объекта, на который указывает
  23. // умный указатель.
  24. //
  25. // При этом все смартпоинтеры, указывающие на один и тот же объект, так же
  26. // будут обращаться к одному и тому же служебному полю, в котором хранится
  27. // количество ссылок на объект, а так же, приписанный объекту способ его
  28. // уничтожения, например стандартный для одиночных указателей, или
  29. // последовательный ( delete [ ] ) на указатель на одномерную непрерывную
  30. // последовательность (массив), благодаря этому всегда, при достижении
  31. // нулевого количества ссылок на удаляемый объект, вызовется правильный
  32. // способ уничтожения.
  33. //
  34. // Локально в классе ссылки и способы уничтожения объектов хранить невозможно
  35. // т.к. это будут отдельные копии данных и вести с ними синхронную работу невозможно
  36. // например объект уже уничтожен, но в однин из экземляров умного указателя по прежнему
  37. // на него указывает
  38. //
  39. // В std::shared_ptr операции изменяющие служебную информацию
  40. // являются "атомарными", т.е. адаптированными для работы в мультипоточности.
  41.  
  42. template <typename T> class SmartPointer
  43. {
  44. private:
  45. T * ptr_;
  46. ControlBlock<T> * state_;
  47. void delete_control ( );
  48. void deleter ( );
  49. public:
  50.  
  51. // конструктор по умолчанию
  52. explicit SmartPointer ( void (*deleterPtr)(T*) = single_delete ) noexcept:
  53. ptr_( nullptr ), state_( new (std::nothrow) ControlBlock<T>(0, deleterPtr) ) { }
  54.  
  55. // конструктор из указателя ( внешнее поведение как у shared_ptr - при исключении
  56. // уничтожает rawPointer )
  57. SmartPointer ( T * rawPointer, void (*deleterPtr)(T*) = single_delete ):
  58. ptr_( rawPointer ), state_(new (std::nothrow) ControlBlock<T>(1, deleterPtr))
  59. {
  60. if ( nullptr == state_ )
  61. {
  62. if ( nullptr != rawPointer ) delete rawPointer;
  63. throw std::bad_alloc();
  64. }
  65. }
  66.  
  67. // конструктор копирования - выделения памяти не происходит, используем уже готовые объекты
  68. SmartPointer ( const SmartPointer<T> & sPointer, void (*deleterPtr)(T*) = single_delete ):
  69. ptr_( sPointer.ptr_ ), state_( sPointer.state_ )
  70. {
  71. state_->count++;
  72. }
  73.  
  74. // деструктор
  75. ~SmartPointer ( ) { deleter(); }
  76.  
  77. // МЕТОДЫ
  78. //
  79. // reset - освобождает память (если хранимый объект существует
  80. // в одном экземпляре, иначе просто -- к числу ссылок
  81. // и устанавливает указатель на новый объект
  82. // use_count - возвращает количество ссылок на объект
  83. //
  84. // get - возвращает указатель на объект. Неободимо для
  85. // случаев, например, таких:
  86. // SmartPointer<int> sp (new int(13));
  87. // int * rp = sp.get();
  88.  
  89. inline void reset ( T* = nullptr, void (*)(T*) = single_delete );
  90. inline unsigned use_count ( ) const { return state_->count; }
  91. inline T * get ( ) const { return ptr_; }
  92.  
  93. // Операторы
  94. T & operator * ( ) { return *ptr_; }
  95. T * operator -> ( ) { return ptr_; }
  96. T & operator [ ] (int i) { return *(ptr_+i); }
  97. T * operator + (int i) { return ptr_+i; }
  98. T * operator - (int i) { return ptr_-i; }
  99.  
  100. // explicit нужен для зашиты, например на случай если забыть реализовать
  101. // один из операторов сравнения, или же, если будут сравниваться несовместимые типы
  102. // на отношение < (код ниже) то при не expicit компилятор увидит невозможность сравнить
  103. // оператором сравнения, но увидит возможность неявно преобразовать к bool и сравнить
  104. // булевы значения нативным оператором сравнения указателей и воспользуется этим.
  105. // Так же explict, способен защитить от возможных проблем неявного
  106. // преобразования в классе наследнике от данного класса.
  107. explicit operator bool( ) const { return nullptr == ptr_ ? false : true; }
  108.  
  109. // for smart_pointer = nullptr or NULL or 0.
  110. SmartPointer<T> & operator = ( std::nullptr_t ) { this->reset(nullptr); }
  111. inline SmartPointer<T> & operator = ( SmartPointer<T> & t );
  112.  
  113. template <class T2> bool operator == (const SmartPointer<T2> & sptr) const
  114. { return ptr_ == sptr.get(); }
  115.  
  116. template <class T2> bool operator != (const SmartPointer<T2> & sptr) const
  117. { return !(*this == sptr); }
  118.  
  119. // данная форма позволят срввнивать некотрые разнотипные, но приводимые
  120. // к друг другу умные указатели
  121. // (например SmartPointer<const int> = SmartPointer<int>)
  122. template <class T2> bool operator <= (const SmartPointer<T2> & sptr) const
  123. { return ptr_ <= sptr.get(); }
  124. // форма оператора, которая позволяет сравнивать умные указатели инстанцированные только
  125. // тем же типом с точностью до const
  126. bool operator < (const SmartPointer<T> & sptr) const // нужна защита explicit на bool()
  127. { return ptr_ < sptr.get(); } // иначе метод может не вызваться
  128.  
  129. bool operator >= (const SmartPointer<T> & sptr) const
  130. { return ptr_ >= sptr.get(); }
  131. template <class T2> bool operator > (const SmartPointer<T2> & sptr) const
  132. { return ptr_ > sptr.get(); }
  133. };
  134.  
  135. template <class T> void SmartPointer<T>::reset(T * pointer, void (*deleterPtr)(T*) )
  136. {
  137. deleter();
  138. try
  139. {
  140. state_ = new ControlBlock<T> ( nullptr == pointer ? 0 : 1, deleterPtr );
  141. }
  142. catch (std::exception & ex)
  143. {
  144. cerr << ex.what() << '\n';
  145. if ( nullptr != state_ ) delete state_;
  146. }
  147. ptr_ = pointer;
  148. }
  149.  
  150. template <class T> SmartPointer<T> & SmartPointer<T>::operator = ( SmartPointer<T> & t )
  151. {
  152. if ( this != &t )
  153. {
  154. // если умные указатели указывают на разные объекты
  155. if ( ptr_ != t.ptr_ )
  156. {
  157. // декремент нужен только тогда когда
  158. // указатель на что-то уже укзывал
  159. if ( nullptr != ptr_ )
  160. --state_->count;
  161.  
  162. // удаляем только когда иссякло количество ссылок
  163. if ( 0 == state_->count )
  164. deleter();
  165.  
  166. // в методах класса можно обращаться к private
  167. // полям другого объекта этого же класса
  168. ptr_ = t.ptr_;
  169. state_ = t.state_;
  170. ++state_->count;
  171. }
  172. // случай присваивания умного указателя, который указывает
  173. // на пустое значение.
  174. else if ( nullptr == t.ptr_ )
  175. {
  176. deleter();
  177. ptr_ = t.ptr_;
  178. state_ = t.state_;
  179. ++state_->count;
  180. }
  181. // если указатели указывают на одну и ту же велечину,
  182. // то тоже ничего делать не нужно
  183. }
  184. return *this;
  185. }
  186.  
  187. template <class T> void SmartPointer<T>::deleter ( )
  188. {
  189. if ( state_->count )
  190. --state_->count;
  191. if ( !state_->count )
  192. delete_control();
  193. }
  194.  
  195. template <class T> void SmartPointer<T>::delete_control ( )
  196. {
  197. if ( nullptr != state_ )
  198. {
  199. // не удаляем, значение, например если это оператор =
  200. if ( nullptr != ptr_ )
  201. {
  202. state_->deleterPtr_(ptr_);
  203. ptr_ = nullptr;
  204. }
  205. delete state_;
  206. state_ = nullptr;
  207. }
  208. }
  209.  
  210. int main()
  211. {
  212. int * raw_pointer1 = new int (69);
  213.  
  214. SmartPointer<int> smart_pointer1(raw_pointer1);
  215.  
  216. cout << smart_pointer1.use_count() << '\n'; // выведет 1
  217. SmartPointer<int> smart_pointer2(smart_pointer1);
  218. cout << smart_pointer1.use_count() << '\n'; // выведет 2
  219.  
  220. cout << *smart_pointer1 << '\n';
  221. cout << *smart_pointer2 << '\n';
  222.  
  223. int * mas = new int [5];
  224. for ( int i=0; i<5; ++i )
  225. mas[i]=i+1;
  226.  
  227. SmartPointer<int> smart_pointer3 ( mas, seq_delete );
  228.  
  229. for ( int i=0; i<5; ++i ) cout << smart_pointer3[i] << ' ';
  230. cout << "\n";
  231. // выведет 1 2 3 4 5
  232.  
  233. // delete raw_pointer; - не нужен
  234. // т.к. объектом теперь владеет умный указатель
  235. // поэтому используем-ка его как указатель
  236. raw_pointer1 = 5 + smart_pointer3.get();
  237.  
  238. for ( int i=4; i>=0; --i ) cout << *(--raw_pointer1) << ' ';
  239. cout << "\n";
  240. // выведет 5 4 3 2 1
  241.  
  242. // #1 вызовется уничтожение последовательности
  243. smart_pointer3 = smart_pointer1;
  244. cout << *smart_pointer3 << '\n';
  245. // выведет 64
  246.  
  247. SmartPointer<string> smart_pointer4 ( new string("Привет") );
  248. // #2 вызовется уничтожение одиночного объекта
  249. smart_pointer4.reset ( new string(*smart_pointer4 + " Медвед") );
  250. cout << *smart_pointer4 << '\n';
  251. // выведет Превед Медвед
  252. SmartPointer<string> smart_pointer5 (smart_pointer4);
  253.  
  254. if (smart_pointer5)
  255. cout << smart_pointer5.use_count() << '\n';
  256. //выведет 2
  257.  
  258. return 0;
  259. }

Замечания. В операторах == и != используется свой шаблон, это необходимо чтобы компилятор выбрал эти операции в случае сравнения разнотипных указателей, и остановил компиляцию на строке с сравнением разнотипных сырых указателей (тем самым дав понять пользователю указателя на ошибку в коде, т.к. в Си++ (и Си) нельзя сравнивать разнотипные указатели), в случае же однотипных – компиляция выполнится и код будет работать. При отсутствии шаблона – тип был бы SmartPointer<T> и найдя, например, такой код:

  1. SmartPointer<int> smart_pointer1(some_value1);
  2. SmartPointer<string> smart_pointer5(some_value2);
  3. If (smart_pointer1 == smart_pointer5) {}

компилятор не выбрал бы операцию ==, но исполнил бы условный блок. А все потому что, компилятор будет пытаться «до последнего» собрать наш код, и увидит что это равенство он может проверить – преобразовав обе переменных к типу bool, а данное преобразование определено над нашим умным указателем. Чтобы такого не происходило, оператор приведения к bool нужно определить со словом explicit – которое запретит неявное преобразование умного указателя к типу bool и возможно будет только явные преобразования, например:

  1. if (smart_pointer1){}

Остальные операторы сравнения указателей показаны как в виде оператора-шаблона с возможностью сравнения с другим типом, так и без. Вариант с дополнительным шаблонным типом более всеяден, и одновременно не нуждается в explicit bool защите. Не шаблонный же вариант нуждается в такой защите. Лучше использовать первый вариант, почему, показано в комментарии в коде.

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

Стоит отметить что использование указателей на функции удалители в данной реализации умного указателя валидно, и такой код более удобочитаем и не приведет к увеличению размера умного указателя, т.к. указатель на функцию хранится в служебном блоке, поэтому размер SmartPointer всегда равен размеру двух указателей, т.к. никаких других полей в нем нет, в этом он похож на std::shared_ptr, но последнем невозможно передавать ф-ии, а только функторы (и лямбды), но функторы же хранятся в служебной информации (пусть и как член данных, который может встроиться, но сами служебные данные - хранятся по-указателю в std::shared_ptr, так что в шареде - функторы не встраиваются. По умолчанию же, если функтор не указан - используется операция delete. А вот для std:unique_ptr можно передать и ф-ю и функтор-удалитель, причем функция увеличит его размер - т.к. будет передана в шаблон как указатель, объект-функтор же, если не имеет никаких полей, кроме операции () - не приведет к увеличению размера, т.к. обращение к нему в бинарном коде будет встроено как вызов встроенной функции.

Заключение

В современном программировании на Си++ крайне рекомендуется, везде где можно пользоваться его новыми средствами - т.е. умными указателями, взамен простых, контейнерами, взамен стандартных массивов и так далее...

Так же, умные указатели выигрывают перед обычными - в случае исключений - Mark1 - "Умные указатели призваны для борьбы с утечками памяти, которые сложно избежать в больших проектах. Они особенно удобны в местах, где возникают исключения, так как при последних происходит процесс раскрутки стека и уничтожаются локальные объекты. В случае обычного указателя — уничтожится переменная-указатель, при этом ресурс останется не освобожденным. В случае умного указателя — вызовется деструктор, который и освободит выделенный ресурс."
Так же в этой статье хорошая рекомендация как избежать утечку при исключении, почему в одном случае она возможна, а в другом нет - в статье.

  1. // возможно утечка
  2. someFunction(std::shared_ptr<Foo>(new Foo), getRandomKey());
  3. // утечка исключена
  4. someFunction(std::make_shared<Foo>(), getRandomKey());

Несколько замечаний по статье Mark1

1. Утверждается что shared_ptr не возможно использовать с массивами. На самом деле возможно, но не приветствуется. Для массивов рекомендуется (4-я глава Эффективного Си++ Майера) использовать контейнеры.

Для того чтобы использовать массивы в shared_ptr необходимо создать функтор удаления ( можно заменить на лямбду, но в Си++11 лямбды еще не могут быть типонезависимыми, таковыми лямбды могут быть только с 14-го стандарта. )

  1. template <class T> class array_deleter
  2. {
  3. public:
  4. void operator () (T *p) {delete [] p; }
  5. };
  6. // если добавить в код выше такой функтор, а в самый низ main такое
  7. // то всё отработает и зачистится хорошо.
  8.  
  9. int * mas2 = new int [5];
  10. for ( int i=0; i<5; ++i )
  11. mas2[i]=i+1;
  12.  
  13. //auto array_deleter = []( int *p ) { delete[] p; };
  14. std::shared_ptr<int> sp( mas2, array_deleter<int>() );

Но есть одна неприятность (видимо чтобы сделать использование shared_ptr не приятным для использования для указания на массивы) в shared_ptr нет операции [] и операций эмулирующих адресную арифметику. Специально так - т.к. использовать более низкоуровневую конструкцию типа моветон, когда есть возможность использовать вектора. [141 страница "Эффективного Си++" 5-го издания, Мейера]

Но если сильно уж хочется, то конечно же можно, таки Си++, сохраняет всю низкоуровневость, при своей высокоуровневости :)

  1. // вариант 1
  2. for ( int i=0; i<5; ++i ) cout << *(&(*sp)+i) << ' ';
  3. // вариант 2
  4. for ( int i=0; i<5; ++i ) cout << (&*sp)[i] << ' ';

Для этого нужно сначала разыменовывать указатель (а он будет ссылаться на первый элемент массива), далее взять его адрес, потом сместить (используя закон адресной арифметики) "бегунок" куда нужно, и все это дело опять разыменовывать. Красота, для фана пойдёт, но в реальном коде - за такое как минимум поругают, причем сильно.

2. Описанную циклическую зависимость проще увидеть при визуализации:

Сначала создается std::shared_ptr<Foo> foo = std::make_shared<Foo>(); - я намеренно указал так вместо auto, т.к. из-за auto не очевидно с каким типом идёт работа.

Умный указатель foo является единственным объектом видимым в main. Далее создается объект Bar, но "ссылка" на него заносится в foo->bar.

Т.к. "по условию задачи" оба объекта ссылаются друг на друга Bar (foo->bar) ссылается на foo (foo->bar->foo = foo;) - при этой строке в служебной информации, описывающей объект Foo количество ссылок устанавливается в 2.

При выходе из main - отрабатывает деструктор foo - уменьшая количество ссылок, но т.к. их остается теперь одна а не ноль - удаление объекта Foo (содержащего динамический объект Bar) не происходит.

И получается утечка памяти. Правда, мне пока не разу не требовался такой шаблон проектирования, где требуется динамический объект хранить в другом объекте, и при этом хранимый должен ссылаться на тот объект, в котором он содержится.

В любом случае для этого, согласно моего текущего опыта, все же удобнее было бы использовать уже стандартные указатели - и в main вести зачистку

  1. delete foo->bar;
  2. delete foo;

Либо следовать рекомендации автора - и использовать в Bar std::weak_ptr. Это возможно из-за того к weak_ptr можно присвоить shared_ptr, а поля ссылок для weak и shared хоть и находятся в одном динамическом объекте - но в разных полях, поэтому такое присваивание не увеличивает количество ссылок, которое используется деструктором foo.

Кстати к weak_ptr можно присвоить shared_ptr (для возможности пользоваться указателем но как бы не владея им (слабо владея) ). Так же к shared_ptr можно присвоить unique_ptr (сделано для использования фабричных ф-й - они т.к. они "могут не знать", будет ли вызывающая их ф-я использовать исключительное или совместное владение) - и вот, если потребуется, такой указатель всегда можно преобразовать в shared_ptr. Другие присваивания между умными указателями различных типов на прямую не возможны.

Намного лучший пример циклических зависимостей и пользы указателя std::weak_ptr приведены у Скотта Мейерса на странице 145, 5-го издания "Эффективного и современного Си++".
Вкратце: в некотором главном объекте каким-то образом хранится в указателях std::shared_ptr "ссылки" на три объекта A, B и С, причем по условию задачи из A и C так же должен быть доступ к B, "предположим, что было бы также полезно иметь указатель из B на А".
weak_ptr
По условию задачи объекты A и C могут перестать указывать на свои объекты в любое время, и если оба перестали - это должно повлечь за собой уничтожение B. Если из B в A обеспечивать "ссылку" через сырой указатель то в B невозможно определить является ли указатель на A "висящим" и при попытке разыменовать - будет неопределенное поведение. Если же использовать std::shared_ptr, то будет описанная выше циклическая зависимость, и в случае если указатель на A переназначается, объект A не удалится, т.к. на него указывает B, и при переназначении C, A и B тоже не удалятся, т.к. при переназначении C число ссылок на B будет не нуль. После переназначений на другие объекты указателей хранящих A и С - в главном объекте произойдет полная потеря созданных объектов A и B (к ним будет невозможно к ним обратиться и уничтожить). Если же в B использовать std::weak_ptr, в отличие от сырого и std::shared_ptr - возможно и определение на висячий указатель, и уничтожение A соответственно. А все потому что конструируясь std::weak_ptr, не увеличивает количество ссылок на указуемый объект.

Пример реализиций умных указателей в GNU

auto_ptr
shared_ptr

Комментарии

Аватар пользователя Илья Лесной

В случае если используется штатный удалитель (т.к. make ф-ии не поддерживают пользовательский) - намного эффективнее использовать для создания умного указателя ф-ии std::make_shared(>=С++11), std::make_unique(>=C++14), т.к. кроме защиты от потенциальных утечек, в случае создания умного указателя в параметрах ф-ии (как показывалось выше) - для shared_ptr, если используется make-функция, компилятор сгенерирует более эффективный код, т.к. в случае создания умного указателя применяется две операции выделения памяти (на сырой указатель, который передается в конструктор умного, и в конструкторе умного - на управляющий блок), а в случае использования make_shared - производится одно выделение, одновременно и для сырого данного, и для служебной информации. Как это работает - можно посмотреть в исходниках. Так же об этом пишет и Мейерс на 149 странице, 5-го издания.

Еще один минус make ф-й, это остутствие поддержки спиcка инициализаторов - std::initializer_list - об этом на 150й и первом абзаце 151 страницы, упомянутой выше книги, вкратце: если есть перегруженная версия конструктора, которая может принимать кол-во параметров схожее по количеству с тем что принял бы конструктор с std::initializer_list, то приоритетнее будет этот конструктор. А конструктор со списком инициализации вызовется только в случае вызова с параметрами в {}, вместо круглых скобок. В make функциях используются круглые скобки.

Так же в этой книге объясняется зачем нужен счетчик для weak_ptr, ведь, казалось бы, для работы weak_ptr использует счетчик shared_ptr. А счетчик же нужен - чтобы деструктор последнего из shared_ptr не уничтожил управляющий блок если в нем счетчик ссылок для weak_ptr не пуст - иначе где-то такой weak_ptr, при попытки обратиться к удаленной служебной информации - скорее всего приведет к segfault. И вот, удаление управляющего блока происходит только тогда - когда все из двух счетчиков - нули, и может быть запущено как и из последнего объекта shared_ptr так и последнего unique_ptr. Обычно, удаление самой указуемой величины происходит, когда из области видимости уходит (или переназначается) последний shared_ptr. А это наталкивает на то, что если в умном указателе shared_ptr нужно хранить объект занимающий много памяти, то make_shared так же лучше не использовать, если в коде возможны висящие указатели unique_ptr время жизни которых дольше... т.к. в таком случае хоть количество shared_ptr уже равно нулю - указуемый большой объект не будет уничтожен, т.к. он хранится в общем блоке памяти со служебной информацией.

Аватар пользователя Илья Лесной

Пример использования умного указателя в одном из паттернов проектирования. В нём хорошо видна возможность Си++ по хранению производного типа, в указателе на базовый тип.