26 December 2011

C++11 умные указатели

Сначала было слово. В смысле, сначала был один единственный умный указатель, просочившийся в стандарт 98-го года -- злополучный std::auto_ptr

Сегодня даже дети знают, насколько это неудачное было решение.
Если говорить коротко, то суть проблемы auto_ptr описывается так: для этого типа разрешено копирование, которое несет в себе серьезный побочный side effect.

Если вы не ищете приключений себе на задницу, никогда так не делаете -- если вы разрешаете для какого-то типа копирование (а как показывает практика, таких типов у вас в программе вряд ли будет сильно больше 5% от общего числа типов), то это копирование должно всегда порождать абсолютно эквивалентную копию. Почему? Да просто потому, что копирование может произойти случайно (забыли & добавить) или не совсем явным образом (как для поля в составе другого типа, для которого копирование разрешено).




По мнению гугловского поиска по картинкам -- умный указатель выглядит примерно так

Потом появились умные указатели в boost, где на смену auto_ptr пришло два новых типа -- scoped_ptr и shared_ptr (на самом деле там их больше, но сейчас мы не будет останавливаться на всяких weak_ptr).

Тип scoped_ptr -- это auto_ptr, для которого запрещено копирование. Вот так вот, просто и понятно.

Тип shared_ptr -- это auto_ptr, для которого реализовано полноценное копирование, т.е. после копирования и оригинальный указатель, и его копия, ссылаются на один и тот же объект; реализовано это все через классическую схему подсчета ссылок, когда только уничтожение последнего указателя на объект, приводит к его уничтожению.
Тонкий нюанс при реализации такого рода указателей -- счетчик ссылок должен быть атомарным для обеспечения корректной работы в многопоточной среде.

Теперь имеем новый стандарт языка, C++11, в который перекочевал shared_ptr, но вот scoped_ptr почему-то заменили на unique_ptr.

Что такое unique_ptr и чем он отличается от scoped_ptr?

Как мы знаем, в новом стандарте языка у типов, наравне с понятием copyable/non-copyable, появилось новое свойство -- movable/non-movable. Т.е. тип можно быть не копируемым, но при этом поддерживать move семантику. Так вот, unique_ptr это scoped_ptr, который стал movable. Т.е. в тех местах, где вы действительно можете безопасно применить разрушающее копирование (пример -- вернуть такой указатель из функции), вы теперь можете это делать. А еще вы наконец-то можете хранить такие указатели в контейнерах, правда, наверное, использовать для этого что-то вроде Boost Pointer Container было бы удобнее.

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

Злоупотребление shared_ptr часто вытекает из функций, которые в качестве своего результата возвращают указатель на созданный внутри себя объект, т.е. передают его во владение наружу. Возвращать при этом auto_ptr не хорошо (просто потому, что auto_ptr в принципе нехорошо использовать, а в новом стандарте он вообще стал depricated). Тип scoped_ptr не вернешь, потому что он некопируемый, поэтому остается два варианта -- либо shared_ptr либо голый указатель.

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

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

Вот теперь все. 
Что еще почитать по теме:


Мудозвоны, писавшие стандарт 98-го года не только засунули туда auto_ptr, но и допустили массу других, не менее очевидных косяков и ляпов.
К нем относится и послабление для сложности метода size() для связанного списка. Его рекомендуют (но не обязывают!), делать со сложностью O(1). Ну и, конечно же, нашлись другие мудаки, которые таки да, реализовали его с линейной сложностью через distance(begin(), end()).

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

Речь, на самом деле, не идет о том, должен ли быть list::size() O(1) или O(n). Если вы считаете, что эффективный splice() сильно важнее размера контейнера -- ради бога, делайте!... Просто метод определения размера тогда, блядь, должен называться не size(), а count_size(). Ну это же очевидно! 

В новом стандарте, кстати, это момент исправили. Метод size() теперь обязательно должен иметь постоянную сложность. И точка. 

No comments:

Post a Comment