Анимация
JavaScript
|
Главная Библионтека 7.5. Стратегии владения Владение объектом является смыслом существования интеллектуальных указателей. Они сами выполняют уничтожение объектов, на которые ссылаются. В то же время пользователь может влиять на продолжительность жизни объекта, вызывая вспомогательные функции. Для реализации прав владения интеллектуальный указатель должен внимательно следить за объектом, особенно во время копирования, присваивания и уничтожения. Это приводит к дополнительным затратам памяти и времени. Приложение должно определять стратегию, которая одновременно была бы удобной и эффективной. В следующих подразделах обсуждаются наиболее распространенные стратегии владения и их реализации в классе SmartPtr. 7.5.1. Гпубокое копирование При копировании интеллектуального указателя простейщая стратегия заключается в копировании объекта, на который он ссылается. При этом на каждый объект будет ссылаться только один указатель. Следовательно, деструктор интеллектуального указателя может безопасно уничтожить такой объект. На рис. 7.1 проиллюстрирована схема распределения памяти для интеллектуальных указателей при глубоком копировании. smartRrI pointee smartRr2
smartRr3
Рис. 7.1. Схема распределения памяти для интеллектуальных указателей при глубоком копировании На первый взгляд стратегия глубокого копирования выглядит бессмысленной. Кажется, что в этом случае интеллектуальные указатели нисколько не расширяют обычную семантику значений языка С++. Зачем же их применять, если объект, на который они ссылаются, можно просто передавать по значению? Ответ заключается в поддержке полиморфизма. Интеллектуальные указатели представляют собой средство для безопасной передачи полиморфных объектов. Интеллектуальный указатель на базовый класс может ссылаться и на производные классы. При копировании интеллектуального указателя требуется копировать и его полиморфное поведение. При этом ни его поведение, ни состояние точно не известны. Поскольку глубокое копирование чаще всего применяется для полиморфных объектов, приведенная ниже наивная реализация оказывается неверной. template <class т> class SmartPtr public: SmartPtr(const SmartPtrS other) : pointee (new T(*other.pointee )) { } Допустим, что мы копируем объект типа smartPtr<widget>. Если интеллектуальный указатель other ссылается на экземпляр класса Extendedwidget, производного от класса Widget, конструктор копирования скопирует только ту часть объекта Extendedwidget, которая унаследована им от класса widget. Это явление известно как срезка (slicing) - копируется только "срез" класса widget, содержащийся в объекте более широкого класса Extendedwidget. Срезка чаще всего совершенно нежелательна. Очень жаль, что в языке С++ она возникает так легко - простой вызов по значению обрезает объекты без всякого предупреждения. В главе 8 обсуждается глубокое клонирование. Классическим способом получения полиморфного клона иерархии является определение виртуальной функции Clone и ее реализация, как показано ниже. class AbstractBase { virtual Base* CloneO = 0; class concrete : public AbstractBase { virtual Base* CloneO { return new Concrete(*this); Реализация функции Clone должна быть одинаковой во всех производных классах. Несмотря на такую повторяющуюся структуру, автоматического способа определения функции-члена clone (кроме макросов) не сушествует. Обобщенный интеллектуальный указатель не знает точно имя функции клонирования: может быть, clone, а может - МакеСору. Следовательно, наиболее гибким подходом является параметризация класса SmartPtr с помощью соответствующей стратегии адресации клонирования. 7.5.2. Копирование при записи Копирование при записи (сору on write - COW) - это способ оптимизации, позволяющий избежать необязательного копирования объекта. Идея этого метода заключается в том, чтобы клонировать объект лишь при первой попытке его модификации, а до тех пор на него может ссылаться несколько указателей. Однако интеллектуальные указатели не очень подходят для реализации стратегии COW, поскольку они не умеют различать вызовы константных и неконстантных функций-членов объекта, на который они ссылаются. Рассмотрим следующий пример. template <c1ass т> class SmartPtr public: т* operator->C) { return pointee ; } class Foo { public: void ConstFunC) const; void NonconstFunC); SmartPtr<Foo> sp; sp->ConstFunC); вызывает оператор ->, a затем - функцию constFun sp->NonConstFunC) вызывает оператор ->, a затем - функцию NonConstFun Для обеих функций вызывается один и тот же оператор ->. Следовательно, интеллектуальный указатель не может решить, применять стратегию COW или нет. Вызовы функций объекта иногда выходят за рамки возможностей интеллектуальных указателей (в разделе 7.11 поясняется, как ключевое слово const влияет на интеллектуальные указатели и объекты, на которые они ссылаются). В заключение отметим, что стратегия COW наиболее эффективна при оптимизации полноценных классов. Интеллектуальные указатели находятся на слишком низком уровне, чтобы применять к ним копирование при записи. Разумеется, они, в свою очередь, могут представлять собой хорошие строительные блоки для реализации стратегии COW в каком-нибудь другом классе. 7.5.3. Подсчет ссылок Подсчет ссылок (reference counting) - наиболее распространенная стратегия владения объектом, используемая интеллектуальными указателями. В рамках этой стратегии производится подсчет интеллектуальных указателей, ссылаюшихся на один и тот же объект. Когда их количество становится равным нулю, объект уничтожается. Эта стратегия работает очень хорошо, если не нарушаются несколько правил - например, на один и тот же объект не должны ссылаться и обычный, и интеллектуальный указатели. Подсчет ссылок должен распространяться только на интеллектуальные указатели. Это приводит к структуре, изображенной на рис. 7.2. Каждый интеллектуальный указатель, кроме самого объекта, хранит указатель на счетчик ссылок (переменную pRef-Count на рис. 7.2). Обычно это приводит к удвоению размера интеллектуального указателя, что может оказаться нежелательным. Сушествует еше один вопрос, связанный с расходом ресурсов. Интеллектуальные указатели с подсчетом ссылок должны хранить в динамической памяти счетчик ссылок. Проблема заключается в том, что во многих реализациях механизм распределения динамической памяти, предусмотренный по умолчанию, работает довольно медленно и неэффективно использует память при выделении ее для небольших объектов (глава 4). (Очевидно, счетчик ссылок, обычно занимаюший 4 байт, следует считать небольшим объектом.) Потеря скорости происходит из-за медленного алгоритма поиска доступных участков памяти (chunks), а затраты памяти вызваны необходимостью хранить информацию о каждом таком участке. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 [ 56 ] 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |