С. Мейерс - Эффективный и современный C++ (1114942), страница 29
Текст из файла (страница 29)
. ." (How do 1 love thee? Letте count the ways.) и Пола Саймона (Paul Simon) с "Наверняка есть 50 способов уйтиот любимого человека" ("There must Ье 50 ways to leave your lover"). Давайте и мы посчитаем причины, по которым так тяжело любить обычные встроенные указатели.l .
Их объявление не дает информации о том, указывают ли они на один объект илина массив.2. Их объявление ничего не говорит о том, должны ли вы уничтожить то, на что онуказывает, когда завершите работу, т.е. владеет ли указатель тем, на что указывает.3. Если вы определили, что должны уничтожить то, на что указывает указатель, нетникакого способа указать, как это сделать. Должны ли вы использовать de l et eили имеется иной механизм деструкции (например, специальная функция уничтожения, которой следует передать этот указатель)?4. Если вам удалось выяснить, что требуется использовать оператор delete, то причина l означает, что нет никакого способа узнать, следует ли использовать оператор для удаления одного объекта (delete) или для удаления массива (de lete [ J ).Если вы используете оператор неверного вида, результат будет неопределенным.5.
Если вы определили, что указатель владеет тем, на что указывает, и выяснили,каким образом уничтожить то, на что он указывает, оказывается очень труднообеспечить уничтожение ровно оди н раз на каждом пути вашего кода (включая те,которые возникают благодаря исключениям). Пропущенный путь ведет к утечкересурсов, а выполнение уничтожения более одного раза - к неопределенному поведению.6. Обычно нет способа выяснить, не является ли указатель висячим, т.е. не указываетли он на память, которая больше не хранит объект, на который должен указыватьуказатель. Висячие указатели образуются, когда объекты уничтожаются, в то время как указатели по-прежнему указывают на них.Встроенные указатели являются острым инструментом, но десятилетия опыта показывают, что достаточно малейшей небрежности или невнимательности - и об этот инструмент можно очень сильно порезаться.Одним из средств решения указанных проблем являются интеллектуальные указатели.
Интеллектуальные указатели представляют собой оболочки вокруг встроенных указателей, которые действуют так же, как и встроенные указатели, но позволяют избежатьмногих связанных с последними ловушек. Поэтому вы должны предпочитать встроеннымуказателям интеллектуальные. Интеллектуальные указатели могут делать почти все то, чтои встроенные указатели, но предоставляют куда меньше возможностей для ошибок.В С + + 1 1 и меются четыре интеллектуальных указателя: s t d : : а u t о _ p t r,std: : unique_pt r, std: : shared_p t r и s t d : : weak_pt r.
Все они разработаны для того,чтобы помочь управлять временем жизни динамически выделяемых объектов, т.е. избежать утечек ресурсов, гарантируя, что такие объекты уничтожаются соответствующимобразом в нужный момент (включая генерацию исключений).Интеллектуальный указатель std : : auto_pt r является устаревшим указателем, доставшимся в наследство от С++98. Попытка его стандартизации привела к тому, что в С++ 1 1он превратился в std : : unique_pt r. Правильное выполнение некоторых работ требовалосемантики перемещения, которой не было в С++98.
В качестве обходного пути был придуман и нтеллектуальный указатель s t d : : auto_pt r, который превращал операцию копирования в перемещение. Это приводило к удивительному коду (копирование std: : auto_pt rпревращало его в нулевой указатель!) и к разочаровывающим ограничениям при использовании (например, было нельзя хранить s t d : : auto_pt r в контейнерах).Интеллектуальный указатель s t d : : unique_ptr делает все то же, что и std : : auto_ptr,плюс еще кое-что. Он делает это максимально эффективно и безо всяких искажений понятия копирования объекта.
Он во всех отношениях лучше s t d : : auto _pt r. Единственный случай обоснованного применения std : : auto_pt rнеобходимость компиляциикода компилятором С++98. Если у вас нет такого ограничения, вы должны заменятьs t d : : auto_pt r указателем s t d : : un ique_ptr.API интеллектуальных указателей на удивление разнообразны.
Практически единственной общей для всех н их функциональностью является наличие конструкторапо умолчанию. Поскольку найти описание этих API несложно, я сосредоточусь на информации, которой часто недостает в описаниях API, например заслуживающие внимания способы применения, анализ стоимости времени выполнения и т.п. Освоение такойинформации может оказаться существенным для разницы между простым использованием интеллектуальных указателей и их эффективным использованием.-4.1 . Испоnьзуйте s td : : unique_ptr дnя управnенияресурсами путем искnючитеnьноrо впаденияКогда вы обращаетесь к интеллектуальным указателям, обычно ближе других под рукой должен находиться s t d : : unique _pt r. Разумно предположить, что по умолчаниюs t d : : un i que_pt r имеет тот же размер, что и обычный указатель, и для большинства1 26Глава 4. Интеллектуальные указателиопераций (включая разыменования) выполняются точно такие же команды.
Это означает, что такие указатели можно использовать даже в ситуациях, когда важны расходпамяти и процессорного времени. Если встроенные указатели для вас достаточно малыи быстры, то почти наверняка такими же будут для вас и указатели s t d : : unique_ptr.Интеллектуальные указатели s t d : : unique_pt r воплощают в себе семантику исключительного вла д ения. Ненулевой s t d : : unique _pt r всегда владеет тем, на что указывает.
Перемещение std : : unique _pt r передает владение от исходного указателя целевому.(Исходный указатель при этом становится нулевым.) Копирование std : : unique_pt r неразрешается, так как если вы можете копировать std : : unique_pt r , то у вас будут дваstd : : un i que_ptr, указывающих на один и тот же ресурс, и каждый из них будет считать,что именно он владеет этим ресурсом (а значит, должен его уничтожить).
Таким образом,st d : : unique_pt r является только перемещаемым типом. При деструкции ненулевойs t d : : uni que_pt r освобождает ресурс, которым владеет. По умолчанию освобождение ресурса выполняется с помощью оператора de lete, примененного ко встроенному указателюв std : : unique ptr.Обычное применение s t d : : unique_pt r - возвращаемый тип фабричных функцийдля объектов иерархии. Предположим, что у нас имеется иерархия типов инвестиций(например, акции, облигации, недвижимость и т.п.) с базовым классом Investment.class Investment { ... } ;class Stock :". } ;puЫic Investmentclass Bond :puЫ ic I nvestment". } ;class RealEstat e :...
} ;puЫic I nvestmentlnvestmentStockBondRealEstateФабричная функция для такой иерархии обычно выделяет объект в динамическойпамяти и возвращает указатель на него, так что за удаление объекта по завершенииработы с ним отвечает вызывающая функция. Это в точности соответствует интеллектуальному указателю std : : unique _pt r, поскольку вызывающий код получает ответственность за ресурс, возвращенный фабрикой (т.е. исключительное владение ресурсом), и s t d : : unique_ptr автоматически удаляет то, на что указывает, при уничтоженииуказателя s t d : : unique_pt r. Фабричная функция для иерархии Investment может бытьобъявлена следующим образом:template<typename . . .
Ts>1 1 Возвращает std : : unique ptr_std: : шU.queytr<Investment>1 1 на объект, созданный изma keinvestment (Ts&& . . . params ) ; / / данных аргументовВызывающий код может использовать возвращаемый s t d : : unique_pt r как в одной области видимости,auto pinvestment4. 1 ./ ! pinvestment имеет типИспользуйте std::uпique_ptr для управления ресурсами путем исключительноrо владения1 27makelnvestment ( a rgшnent s ) ; / / std : : uni que_ptr<Investment>/ / Уничтожение *plnvestmentтак и в сценарии передачи владения, таком, как когда std : : unique_pt r, возвращенныйфабрикой, перемещается в контейнер, элемент контейнера впоследствии перемещаетсяв член-данные объекта, а этот объект позже уничтожается. Когда это происходит, членданные std : : un i que_pt r объекта также уничтожается, что приводит к освобождениюресурса, полученного от фабрики.
Если цепочка владения прерывается из-за исключенияили иного нетипичного потока управления (например, раннего возврата из функции илииз-за break в цикле), для std : : unique_ptr, владеющего ресурсом, в конечном итоге вызывается деструктор1, и тем самым освобождается захваченный ресурс.По умолчанию это освобождение выполняется посредством оператора delete, нов процессе конструирования объект s t d : : un i que_p t r можно настроить для использования пользовательских уд алителей (custom deleters): произвольных функций (илифункциональных объектов, включая получающиеся из лямбда-выражений), вызываемыхдля освобождения ресурсов. Если объект, созданный с помощью ma keinvestment, недолжен быть удален непосредственно с помощью delete, а сначала должна быть внесеназапись в журнал, ma kei nvestment можно реализовать следующим образом (поясненияприведены после кода, так что не беспокойтесь, если вы увидите что-то не совсем очевидное для вас).auto del lnvmt=[]( Investment* plnvestment ) / / Пользовательский11 удалитель .makeLogEntry (pinvestment ) ; // (Лямбда - выражение )delete plnvestment ;};template<typename .