Анимация
JavaScript


Главная  Библионтека 

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

7.3. Хранение интеллектуальных указателей

Для начала зададимся фундаментальным вопросом: "Должна ли переменная poi ntee иметь тип т*?". Если нет, то какой тип она может иметь? В обобшенном профаммировании обязательно нужно задавать себе такие вопросы. Каждый тип, встроенный в обобшенный код, снижает степень его универсальности. Такие типы напоминают константы, "зашитые" в тексте профаммы.

В некоторых ситуациях имеет смысл разрешить настройку типа переменной pointee . Например, это стоит сделать при работе с нестандартными модификаторами указателя. Во времена 16-битовых процессоров Intel 80x86 указатели могли иметь спецификаторы пеаг, far и huge. Такие модификаторы могут использоваться и в других моделях сегментированной памяти.

Вторая ситуация, в которой следует настраивать тип, возникает, когда профаммист хочет применить наследование интеллектуальных указателей. Что если у нас есть класс LegacySmartPtr<T>, реализованный кем-то еше, и нам нужно создать производный от него класс? Как это сделать? Это ответственное решение. Лучше скрыть базовый класс внутри своего собственного интеллектуального указателя. Это вполне возможно, поскольку внутренний интеллектуальный указатель имеет ту же синтаксическую структуру. С точки зрения внешнего интеллектуального указателя переменная pointee имеет тип LegacySmartPtr<T>, а не т*.

Наследование интеллектуальных указателей обладает интересными приложениями, в основном благодаря оператору ->. Когда оператор -> применяется к типу, который отличается от встроенного, компилятор ведет себя необычно. После применения оператора ->, определенного пользователем, к данному типу он вновь применяет оператор -> к полученному результату и продолжает рекурсивно повторять этот процесс, пока не обнаружит указатель на встроенный тип и только тогда предоставит доступ к члену класса. Следовательно, оператор ->, определенный в интеллектуальном указателе, не обязан возврашать указатель. Он может возврашать объект, который в свою очередь реализует оператор ->, не изменяя синтаксис его применения.

Это приводит к интересной идиоме: вызовам пре- и постфункций (Stroustmp, 2000). Если оператор -> возвращает объект типа PointerType по значению, последовательность его выполнения такова.

1. Вызывается конструктор класса PointerType.

2. Вызывается функция PointerType: :operator->; вероятно, возвращается указатель на объект типа PointeeType.

3. Доступ к члену класса PointeeType - вероятно, вызов функции.

4. Вызывается деструктор класса PointerType.

Короче говоря, у нас есть превосходный способ блокировки вызовов функций. Эта идиома широко применяется в многопоточных средах для блокировки доступа к ресурсам. Конструктор класса PointerType может захватывать ресурсы, мы их можем использовать в своих целях, а в конце работы десфуктор класса PointerType освобождает их.

На этом обобщения не заканчиваются. Синтаксически-ориентированная часть "указателя" выглядит довольно бледно по сравнению с мощными средствами управления ресурсами, которыми обладают интеллектуальные указатели. Следовательно, иногда интеллектуальные указатели могут вести себя подобно обычным. Объект, для которого не определены операторы -> и *, не соответствует определению интеллекту-



ального указателя. Однако существуют объекты, которые можно считать интеллектуальными указателями, даже если они таковыми формально не являются.

Вернемся в реальный мир прикладного программирования и приложений. Многие операционные системы реализуют дескрипторы (handles) в виде методов доступа к определенным внутренним ресурсам, таким как окна, мьютексы или устройства. Дескрипторы являются преднамеренно замаскированными указателями. Одно из их предназначений - предотвращать непосредственный доступ пользователей к критически важным ресурсам операционной системы. В больщинстве случаев дескрипторы представляют собой целые числа, указывающие на скрытую таблицу указателей. Эта таблица обеспечивает дополнительный уровень защиты внутренней системы от прикладных программистов. Хотя для дескрипторов не предусмотрен оператор ->, по семантике и способу работы они похожи на указатели.

Для дескрипторов нет смысла предусматривать оператор -> или *. Однако они предоставляют те же средства управления ресурсами, что и интеллектуальные указатели.

Рассмотрим три типа интеллектуальных указателей.

1. Тип хранения (storage type). Это- тип переменной pointee . По умолчанию в обычных интеллектуальных указателях этот тип относится к обычным указателям.

2. Тип указателя (pointer type). Это тип объекта, возвращаемого оператором ->. Он может отличаться от типа хранения, если хранится объект-заместитель (proxy object), а не сам указатель. (Позднее в этой главе мы рассмотрим пример объекта-заместителя.)

3. Ссылочный тип (reference type). Это тип, возвращаемый оператором *.

В заключение заметим, что интеллектуальные указатели могут и должны обобщать все три перечисленных типа. Для этого класс SmartPtr абстрагирует их в стратегии Storage. Конкретизация класса SmartPtr не обязана реализовывать их все одновременно. Следовательно, иногда (например, в дескрипторах), стратегия может не определять один из операторов -> и *, либо игнорировать их обоих.

7.4. Функции-члены интеллектуальных указателей

Многие существующие реализации интеллектуальных указателей допускают выполнение операций с помощью функций-членов, например. Get для доступа к объекту. Set - для его изменения и Release - для отказа от владения. Вполне естественно инкапсулировать эти функциональные возможности в классе SmartPtr.

Однако опыт показывает, что функции-члены не очень удобны для реализации интеллектуальных указателей, поскольку взаимодействие между этими функциями крайне запутанно.

Допустим, что у нас есть класс Printer с функциями-членами Acquire и Release. С помощью функции-члена Acquire мы получаем права владения принтером, так что никакое другое приложение ничего напечатать не сможет, а с помощью функции-члена Release мы освобождаем принтер. Используя интеллектуальный указатель на объект класса Printer, мы обнаруживаем его странную синтаксическую схожесть с конструкциями, имеющими соверщенно иную семантику.

SmartPtr<Printer> spRes =

spRes->Acquire(); вступаем no владение принтером ... Печатаем документ ...



spRes->Release(); Освобождаем принтер spRes.ReleaseC); Освобождаем указатель на принтер

Пользователь класса SmartPtr теперь имеет доступ к двум совершенно разным множествам функций: функциям-членам объекта, на который ссылается интеллектуальный указатель, и функциям-членам самого интеллектуального указателя. Выбор этих функций зависит от используемого оператора: точки или стрелки.

Профаммисты на языке С++ вынуждены внимательно следить за тем, какой из операторов должен применяться в той или иной ситуации. Профаммисты на языке Pascal, изучающие язык С++, могут даже почувствовать отвращение к необходимости улавливать различия между операциями & и &&. Однако профаммисты на языке С++ не имеют права закрывать на это глаза. Они должны выработать в себе привычку легко обнаруживать такие синтаксические различия.

Привыкнуть к функциям-членам интеллектуальньк указателей нелегко. Простые указатели не имеют функций-членов, поэтому операторы . и -> к ним не применяются. В этих ситуациях большую помошь оказывает компилятор: если профаммист поставил после обычного указателя точку, вьщается сообщение об ошибке. Теперь легко себе представить, как разочарованы даже опытные профаммисты на языке С++ тем фактом, что компилятор допускает использование выражений sp.ReleaseC) и sp->Release(), имеющих совершенно разный смысл. Избежать этого легко: интеллектуальный указатель не должен иметь функций-членов. Класс SmartPtr использует вместо них дружественные функции.

Перефуженные функции также могут привести к недоразумениям, но ситуация здесь соверщенно иная. В языке С++ перефуженные функции применяются довольно широко. Перефузка является важной частью языка и часто применяется в библиотеках и приложениях. Это значит, что профаммисты на языке С++ привыкли дифференцировать разные синтаксические формы вызова, например, Release(*sp) и Release(sp).

Функциями-членами класса SmartPtr могут быть лишь конструкторьс, деструктор, оператор =, оператор -> и унарньсй оператор *. Остальные операции, выполняемые классом SmartPtr, реализуются с помощью внешних функций.

Для простоты класс SmartPtr не использует именованные функции-члены. Единственными функциями, имеющими доступ к объекту, на который ссылается интеллектуальный указатель, являются Getlmpl, GetlmplRef, Reset и Release, определенные в пространстве имен.

template <class т> т* Getlmpl(SmartPtr<T>& sp); template <class T> T*& GetlmplRef(SmartPtr<T>& sp); template <class T> void Reset(SmartPtr<T>& sp, т* source); template <class T> void Release(SmartPtr<T>cb sp, T*& destination);

• Функция Getlmpl возвращает указатель на объект, хранящийся в классе SmartPtr.

• Функция Getlmpl Ref возвращает ссылку на указатель, хранящийся в классе SmartPtr. Она позволяет изменять этот указатель, поэтому требует особой осторожности.

• Функция Reset возвращает указатель на другое значение, освобождая предьщущее.

• Функция Release освобождает интеллектуальный указатель, возлагая на пользователя ответственность за управление объектом, на который он ссылался.

Реальные объявления этих функций в библиотеке Loki немного сложнее. В них не предполагается, что указатель, хранящийся в классе SmartPtr, имеет тип т*. Как указано в разделе 7.3, тип указателя определяется стратегией Storage.



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