А. Александреску - Современное проектирование на C++ (1119444), страница 46
Текст из файла (страница 46)
189 Глава 7. Интеллектуальные указатели Рис. 7.5. Связывание ссы,гви Преимушество связывания ссылок перед их подсчетом проявляется в том, что при первом способе не требуется дополнительная память. Это делает стратегию более надежной. Создание интеллектуального указателя со связыванием ссылок всегда работает безотказно. Недостатком этой стратегии является тот факт, что при связывании необходимо больше памяти для регистрации (три указателя вместо одного плюс одно целое число). Кроме того, подсчет ссылок выполняется немного быстрее — при копировании интеллектуального указателя необходима только одна косвенная адресация и одна операция инкрементации. Управление списком также немного сложнее.
В заключение отметим, что связывание ссылок следует применять только при недостатке динамической памяти. В противном случае предпочтительнее использовать подсчет ссылок. Завершая обсуждение стратегий управления ссылками, обратим внимание на их существенный недостаток. Управление ссылками — с помощью подсчета или связывания — приводит к утечке ресурсов, так называемой циклической ссылке (сус)гс геГегепсе). Представьте себе, что объект а содержит интеллектуальный указатель на объект в, и, наоборот, — в объекте в хранится интеллектуальный указатель на объект ж Эти два объекта образуют циклическую ссылку.
Даже если вы не используете эти объекты, они сами друг друга используют. Стратегии управления ссылками не способны распознавать такие ситуации, и эти два объекта останутся в динамической памяти навсегда. Циклы могут распространяться на несколько объектов, создавая между ними связи, которые очень трудно отследить. Несмотря на это, управление ссылками представляет собой надежную и быструю стратегию влаления объектами. Если применять его с необходимыми предосторожностями, оно значительно облегчает разработку приложений. 7.5.5. Разрушающеекопироаание Разрушающее копирование (деыгцсггве сору) — это именно то, о чем вы подумали: во время копирования оригинал уничтожается.
Разрушающее копирование уничтожа- 190 Часть!й Компоненты ет оригинальный указатель, передавая объект, на который он ссылался, другому интеллектуальному указателю. В шаблонном классе зстт:: восо рсг применяется именно этот вид копирования. Опасность этой стратегии очевидна. Неправильное разрушаюшее копирование может уничтожить данные, программу и вашу репутацию программиста. Эту стратегию следует применять только в том случае, когда в каждый момент времени на данный объект ссылается только один интеллектуальный указатель. Во время копирования или присваивания одного интеллектуального указателя другому "в живых" остается только результируюший указатель, а оригинал обнуляется.
Ниже показаны конструктор копирования и оператор присваивания простого класса 5вагСРСг, демонстрируюшие разрушаюшее копирование. севр1асе <с1аээ т> с1аьэ 5вагсрсг ( риЫт'с: 5вагсрсг(5вагсрсгб эгс) ( ро1псее = эгс.ротпсее эгс.ро1псее = О; 5вагсРсг4 орегасог=(5вагсрсг4 эгс) ( ту (стт(5 1= йэгс) ( бе1есе Розпсее ротпсее = эсс.ро1псее эгс.розлсее = О; гесогп *сот'э; По правилам языка С++ в правой части конструктора копирования и оператора присваивания должна стоять ссылка на константный объект. Классы, реализующие разрушаюшее копирование, нарушают это правило по очевидным причинам. Поскольку правила языка основаны на резонных соображениях, от их нарушения нельзя ожидать ничего хорошего. Действительно, рассмотрим следующий пример.
чост) Отэр1ау(5вагСРСг<5овеСЫпд> эр); 5вагсрс г<5овеспз пй> эр(пеи 5овесЫ ой); о)эр1ау(эр); // уничтожает объект эр Несмотря на то что функция от эр1ау не представляет опасности для своего аргумента (принимая его по значению), она действует как водоворот; все интеллектуальные указатели, попавшие в нее, тонут безвозвратно. После вызова р)эр1ау(зр) указатель зр хранит нулевой адрес. Поскольку интеллектуальные указатели, реализуюшие стратеппо разрушаюшего копирования, не поддерживают семантику значений, их нельзя хранить в контейнерах и вообше с ними нужно обрашаться почти так же осторожно, как и с обычными указателями.
Возможность хранить интеллектуальные указатели в контейнере чрезвычайно важна, поскольку контейнеры, состояшие из обычных указателей, очень усложняют ручное управление владением. Однако интеллектуальные указатели, реализуюшие стратегию разрушаюшего копирования, для этого не подходят. 191 Глава 7. Интеллектуальные указатели С другой стороны, интеллектуальные указатели, реализующие стратегию разрушающего копирования, имеют ряд преимуществ. ° Они почти не тратят дополнительной памяти.
в Они удобны для передачи владения другому указателю. В этом случае используется "эффект водоворота", описанный выше. ° Они удобны в качестве возвращаемого функцией значения. Если в реализации интеллектуального указателя используется определенный трюк', можно возвращать из функций интеллектуальные указатели с разрушающим копированием. Таким образом, можно быть уверенным, что если в вызывающем модуле возвращаемое значение не используется, оно уничтожается. ° Они превосхолны в качестве переменных стека в функциях, возвращающих значения несколькими способами. Предпринимать меры для уничтожения объекта, на который ссылается интеллектуальный указатель, необязательно — он сам это сделает.
Стратегия разрушающего копирования используется в стандартном классе зсдз:аосо рсг. Это создает дополнительное преимушество. ° Интеллектуальные указатели, обладающие семантикой разрушающего копирования, являются единственным стандартом. Это значит, что многие программисты рано или поздно станут нх использовать. По этим причинам реализация класса 5пагтгртг должна предусматривать дополнительную поддержку семантики разрушающего копирования. Интеллектуальные указатели используют разные стратегии владения, каждая из которых имеет свои достоинства и недостатки.
К наиболее важным приемам относятся глубокое копирование, подсчет ссылок, связывание ссылок и разрушающее копирование. Класс 5магтртг реализует все эти сПособы в виде стратегии ОппегзЫр, позволяя своим пользователям выбирать из них наиболее подходящий. По умолчанию предлагается стратегия подсчета ссылок. 7.6. Оператор взятия адреса Стремясь сделать интеллектуальные указатели максимально похожими на их обычные прототипы, разработчики натолкнулись на незаметный перегружаемый унарный оператор бь или оператор взлпзил адреса (аддгезв-оГ орегагог)з.
Программист, реализующий интеллектуальные указатели, может перегрузить этот оператор слелуюшим образом. темр1ате <с1авв т> с1авв 5яагтртг ( риЬ1з с: т"* орегатогйО гетогп брозптее з Изобретенный Грегом Колвнном (Стев Со1ып) н Биллом Гнббонсом (Врй СВЬЬопз) длв стандартного интеллектуального указателя втд::анто ртг. з Унсримй оператор б следует отличать от бинарного оператора й, представляющего собой побитовый оператор АН0. 19г Часть 11.
Компоненты Помимо всего прочего, если интеллектуальный указатель должен имитировать обычный указатель, то его адрес можно заменять адресом обычного указателя. В этом случае становится возможной следующая перегрузка оператора. чозд Рип(и1ддес*" ризддех); БаагеРггказ ддее> за ддее(... ); Рип(бзрадддет); // Вызывает оператор ' и получает указатель // на указатель на объект класса и1ддет На первый взгляд было бы замечательно иметь полную совместимость интеллектуальных и обычных указателей, однако перегрузка унарного оператора 4 относится к тем остроумным трюкам, которые приносят больше вреда, чем пользы.
Есть лве причины, по которым перегрузка унарного оператора б опасна. Первая заключается в том, что явное определение адреса обьекта, на который ссылается интеллектуальный указатель, отнимает возможности автоматического управления владением. Если пользователь имеет свободный доступ к адресу указателя, любая вспомогательная структура, содержащаяся в интеллектуальном указателе, например, счетчик ссылок, становится неработоспособной. Если пользователь непосредственно оперирует адресом обычного указателя, интеллектуальный указатель становится полностью неуправляемым. Вторая причина более прагматична.
Дело в том, что перегрузка унарного оператора б делает невозможным использование интеллектуального указателя в сочетании с контейнерами из стандартной библиотеки шаблонов Вть, Фактически перегрузка унарного оператора б не позволяет применять обобщенное программирование, поскольку адрес любого объекта — слишком важное свойство, чтобы так просто с ним обращаться, В большинстве обобщенных кодов предполагается, что применение оператора б к объекту типа т возвращает объект типа т".
Как видим, оператор взятия адреса представляет собой фундаментальное понятие обобщенного программирования. Если им пренебречь, обобщенный код будет вести себя странно как на этапе компиляции, так и (что намного хуже) во время выполнения программы. Таким образом, перегружать унарный оператор б для интеллектуальных указателей и вообще для любых объектов не рекомендуется, поэтому в классе Ввагтргг унарный оператор б не перегружается.
7.7. Неявное приведение к типам обычных указателей Рассмотрим следующий код. чо?д Рцп(зоаетм пд* р) Ваагтятг(воветп1пд> зр(пеа ВоветЫпд); Рцп(зр); // правильно или нет? Скомпилируется этот код или нет? По принципу "максимальной совместимости" правильный ответ — "да". С технической точки зрения сделать приведенный выше код компилируемым очень легко, Для этого достаточно ввести преобразование, определенное пользователем, тевр1ате <с1азз т> с1азз БаагтРтг 193 Глава 7. Интеллектуальные уквзатвли риЫ(с: орегатог т*о // приведение к типу т*, // определенное пользователем ( гетигп ро(птее Однако это еше не все.
Преобразования типов, определенные пользователем, имеют весьма интересную историю. В 1980-х голах, когда они впервые появились в языке С++, большинство программистов считали их огромным достижением. Они позволяли разрабатывать единообразные системы типов, повышать выразительность семантики и определять новые типы, отличающиеся от встроенных. Однако впоследствии оказалось, что преобразования типов, определенные пользователем, неудобны и потенциально опасны.
Они могут стать опасными особенно при обработке внутренних данных (Меуегз, !998а. 11еш 29). Именно это происходит с типом т* в приведенном выше коле. Вот почему следует хорошенько подумать, прежде чем допускать автоматическое преобразование интеллектуальных указателей в вашей программе. Одна из потенциальных опасностей происходит от неконтролируемого доступа пользователя к обычному указателю, который скрыт внутри интеллектуального.
Передача обычного указателя во внешнюю среду нарушает внутреннюю рабату интеллектуального указателя. Вырвавшись из своей оболочки, обычный указатель может легко с~а~ь угрозой для нормального функционирования программы, как это было до появления интеллектуальных указателей. Лругая опасносгь исходит от непредвиденного выполнения преобразований, определенных пользователем, даже когда они совершенно не нужны. Рассмотрим следующий пример. 5шагтятг<5ошетЫпй> зр; // грубая семантическая ошибка.