А. Александреску - Современное проектирование на C++ (1119444), страница 45
Текст из файла (страница 45)
184 Часть П. Компоненты 7.5. Стратегии владения Владение объектом является смыслом сушествования интеллектуальных указателей. Они сами выполняют уничтожение объектов, на которые ссылаются. В то же время пользователь может влиять на продолжительность жизни объекта, вызывая вспомогательные функции. Для реализации прав владения интеллектуальный указатель должен внимательно следить за объектом, особенно во время копирования, присваивания и уничтожения. Это приводит к дополнительным затратам памяти и времени.
Приложение должно определять стратегию, которая одновременно была бы удобной и эффективной. В следуюших подразделах обсуждаются наиболее распространенные стратегии владения и их реализации в классе Эваггртг. 7.б. 7. Глубокое копирование При копировании интеллектуального указателя простейшая стратегия заключается в копировании объекта, на который он ссылается. При этом на каждый объект будет ссылаться только один указатель.
Следовательно, деструктор интеллектуального указателя может безопасно уничтожить такой объект. На рис. 7.1 проиллюстрирована схема распределения памяти для интеллектуальных указателей при глубоком копировании. эта йргг! нпгаПРГГ2 апаПРГГЭ Рис. 7.1. Схема распределения пимяти для интеллектуальнмх указателей при глубоком копировании На первый взгляд стратегия глубокого копирования выглядит бессмысленной. Кажется, что в этом случае интеллектуальные указатели нисколько не расширяют обычную семантику значений языка С++.
Зачем же их применять, если объект, на который они ссылаются, можно просто передавать по значению? Ответ заключается в поддержке полиморгризма. Интеллектуальные указатели представляют собой средство лля безопасной передачи полиморфных объектов. Интеллектуальный указатель на базовый класс может ссылаться и на производные классы.
При копировании интеллектуального указателя требуется копировать и его полиморфное поведение. При этом ни его поведение, ни состояние точно не известны. Поскольку глубокое копирование чаше всего применяется для полиморфных объектов, приведенная ниже наивная реализация оказывается неверной. тетр1ате <с1ааа т с1ааа зтагсрсг 185 Глава7. Интеллектуальные указатели с рОЫ1с: ввагтятг(сопэт ваагтртгй отйег) розптее (пен т(*оъбег.ро1птее )) ) Допустим, что мы копируем объект типа ваагтртг<нзддет>. Если интеллектуальный указатель оьбег ссылается на экземпляр класса вхтепдег)н!г)дет, производного от класса нз Одет, конструктор копирования скопирует только ту часть объекта яхгепдег)нзддег, которая унаследована им от класса изыдет. Это явление известно как слезка (з!1сшд) — копируется только "срез" класса из г)дет, содержащийся в объекте более широкого класса дхтепоедн1ддет.
Срезка чаше всего совершенно нежелательна. Очень жаль, что в языке С++ она возникает так легко — простой вызов по значению обрезает объекты без всякого предупреждения. В главе 8 обсуждается глубокое клонирование. Классическим способом получения полиморфного клона иерархии является определение виртуальной функции С1опе и ее реализация, как показано ниже. с1аьа АЬэтгаствазе чт гсца1 вазе* с1опео = О; с1аьа сопсгете : риЫ!с лЬасгаствазе ( чтгтца! вазе* с1опеО ( гетогп пеп сопсгете(*тбзэ); Реализация функции с1опе должна быть одинаковой во всех произволных классах. Несмотря на такую повторяющуюся структуру, автоматического способа опрелеления функции-члена с1опе (кроме макросов) не существует.
Обобщенный интеллектуальный указатель не знает точно имя функции клонирования: может быть, с1опе, а может — макесору. Следовательно, наиболее гибким подходом является параметризация класса Ваагтятг с помощью соответствующей стратегии адресации клонирования. ?.о.2. Копирование при записи Копирование при записи (сору оп чт!ге — СО%) — это способ оптимизации, позволяющий избежать необязательного копирования объекта. Идея этого метода заключается в том, чтобы клонировать объект лишь при первой попытке его модификации, а до тех пор на него может ссгялаться несколько указателей. Однако интеллектуальные указатели не очень подходят для реализации ст)ютегии СО%, поскольку они не умеют различать вызовы константных и неконстантных функций- членов объекта, на который они ссылаются.
Рассмотрим следуклций пример. севр1ате <с1аьа т> с!аьа Веагтртг 166 Часть)). Компоненты ( риЫ1с: т" орегатог->О ( гетигп розптее с)а55 ГОО риЫ1с: УО10 СопзтГипо соп5т; ЧО1О ЙОПСОП5тГип(); 5вагтРтг<ГОО> зр; зр->сопзггипО; // Вызывает оператор ->, // а затем — функцию сопзтГип зр->нопсопзтГОПО // Вызывает оператор ->, // а затем — функцию мопсопзтГип Для обеих функций вызывается один и тот же оператор ->. Следовательно, интеллек- туальный указатель не может решить, применять стратегию СО5У или нет. Вызовы функций объекта иногда выходят за рамки возможностей интеллектуальных указате- лей (в разделе 7.11 поясняется, как ключевое слово сопзт влияет на интеллектуальные указатели и объекты, на которые они ссылаются). В заключение отметим, что стратегия СОУУ наиболее эффективна при оптимиза- ции полноценных классов.
Интеллектуальные указатели находятся на слишком низ- ком уровне, чтобы применять к ним копирование при записи. Разумеется, они, в свою очередь, могут представлять собой хорошие строительные блоки для реализации стратегии СОхч в каком-нибудь другом классе. 7.5.3. Подсчет ссылок Подсчет ссылок (геГегепсе соипйпв) — наиболее распространенная стратегия влаления объектом, используемая интеллектуальными указателями. В рамках этой стратегии производится подсчет интеллектуальных указателей, ссылаюшихся на один и тот же объект. Когда их количество становится равным нулю, объект уничтожается.
Эта стратеп1я работает очень хорошо, если не нарушаются несколько правил — например, на алин и тот же обьект не должны ссылаться и обычный, и интеллектуальный указатели. Подсчет ссылок должен распространяться только на интеллектуальные указатели. Это приводит к структуре, изображенной на рис. 7.2. Каждый интеллектуальный указатель, кроме самого объекта, хранит указатель на счетчик ссылок (переменную ряебсоипт на рис. 7.2). Обычно это приводит к удвоению размера интеллектуального указателя, что может оказаться нежелательным.
Сушествует еше один вопрос, связанный с расходом ресурсов. Интеллектуальные указатели с подсчетом ссылок должны хранить в линамической памяти счетчик ссылок. Проблема заключается в том, что во многих реализациях механизм распределения динамической памяти, предусмотренный по умолчанию, работает довольно медленно и неэффективно использует память при выделении ее для небольших обьектов (глава 4). (Очевидно, счетчик ссылок, обычно занимаюший 4 байт, следует считать небольшим объектом.) Потеря скорости происходит из-за медленного алгоритма поиска доступных участков памяти (с11ипЫ), а затраты памяти вызваны необходимостью хранить информацию о каждом таком участке. 187 Гпава7.
Интеллектуальные указатели Относительный перерасход памяти может частично компенсироваться совместным хранением указателя и счетчика ссылок, как показано на рис. 7.3. Структура, изображенная на рис. 7.3, позволяет сократить размер интеллектуального указателя до размера обычного указателя, но за счет скорости доступа: обьект, на который ссылается интеллектуальный указатель создает дополнительный уровень косвенной адресапии. Это довольно значительный недостаток, поскольку обычно интеллектуальные указатели используются несколько раз, а создаются и уничтожаются — лишь однажды.
Рис. 7.л. Три интеллектуальных указателя, ссылающихся на один и тот лсе адьект Рис. 73. Альтернативная структура указателей с нодсчетои ссылок Эффективнее всего хранить счетчик ссылок в самом объекте, на который ссылается интеллектуальный указатель, как показано на рис. 7.4. Таким образом, объект класа дтагтртг будет иметь размер обычного указателя, и дополнительных затрат памяти не будет совсем. Этот прием называется внедренным подсчетом ссылок 8пггомче гсрегепсе соопппя), поскольку счетчик ссылок "внедряется" в объект, хотя семантиче- 188 Часть Н.
Компоненты ски он относится к интеллектуальному указателю. Его название напоминает также об ахиллесовой пяте этого приема: для реализапии такой стратегии нужно моди4>ицировать класс, которому принадлежит объект. Рис. 7.4. Внедренный наденем ссылок Обобшенный интеллектуальный указатель должен при первой возможности использовать внедренный подсчет ссылок, а обычную схему подсчета ссылок следует рассматривать в качестве приемлемой альтернативы.
)1ля реализации обычной схемы подсчета ссылок очень удобно применять механизм распределения памяти для небольших объектов, описанный в главе 4. Он позволяет снижать дополнительные затраты, вызванные необходимостью хранить счетчик ссылок. 7.6.4. Связывание ссылок На самом деле нет никакой необходимости подсчитывать интеллектуальные указатели, ссылаюшиеся на один и тот же объект. Нужно лишь определять, когда этот счетчик станет равным нулю, Это приводит к идее "списка владельпев" (оччпегв)1!р (!и), показанного на рис. 7.5,' и стратегии связывания ссылок. Все объекты класса 5щагтртг, ссылающиеся на заданный объект, заносятся в дважды связанный список. Новый объект класса 5щагтртг, создаваемый на основе суШествуюшего, добавляется в список, а деструктор этого класса удаляет уничтоженные объекты из списка.
Когда список становится пустым, объект удыяется. Структура дважды связанного списка идеально подходит для отслеживания ссылок. Использовать односвязный список невозможно, поскольку удаление элемента из такого списка занимает линейное время. Вектор применить нельзя, так как объекты класса 5еагтРтг не занимают смежные участки памяти (и на удаление элементов из вектора также тратится линейное время). Нам нужна структура, в которой удаление и вставка элементов. а также проверка заполненности занимали бы одинаковое время.
Единственной структурой, удовлетворяюшей этим требованиям, является дважды связанный список. В реализапии стратегии связывания ссылок в каждом объекте класса 5еагтртг хранятся два дополнительных указателя — на следуюший и на предыдущий элементы. ' Механизм связывания ссылок описан Рното Лаикинсном (йсло !.ап!с!псп) в бюрумс ()еспс~ в ноябре (995 года.