А. Александреску - Современное проектирование на C++ (1119444), страница 47
Текст из файла (страница 47)
// однако компилятор ее не распознает де1ете зр; Компилятор сопоставляет оператор де1ете с преобразованием к типу т*, определенным пользователем. Во время выполнения программы вызывается оператор т*, и к результату его работы применяется оператор ое1ете. Очевидно, это совершенно не то, что требуется от интеллектуального указателя, поскольку предполагается, что он сам управляет правами владения.
Лишний и непреднамеренный вызов оператора де1ете нарушает всю тщательно отлаженную систему управления правами владения, скрытую внутри интеллектуального указателя. Есть несколько способов преодотвратить компиляцию вызова оператора ое1ете. Некоторые из них очень остроумны (Меуегз, 1996). Эффективнее и улобнее всего сделать вызов оператора г(е1ете внутренне неоднозначным (ать(йиоиз). этого можно достичь, предусмотрев два приведения к типам, реагирующих на вызов оператора бе1ете Один из типов — это сам тип т*, а вторым может быть тип чо(0=. сешр1ате <с1азз т> с1аьа 5шагтптг риЫ(с: орегатог Т*о // Приведение к типу Т», // определенное пользователем 194 Часть 1!.
Компоненты ( гесцгп ро))псее орегасог чозд"О // добавляется приведение к типу чосд* ( гесцгп роспсее ; Применение оператора де1есе к интеллектуальному указателю неоднозначно. Компилятор не сможет решить, какое из преобразований следует выполнить. Именно этого мы и добивались.
Не забывайте, что блокировка оператора де1есе — это только часть проблемы. Остается решить, применять ли автоматическое преобразование интеллектуального указателя в простой указатель. Разрешить — слишком опасно, запретить — очень легко. Окончательная реализация класса 5 пагсясг предоставляет программисту право выбора.
Однако запрет неявного преобразования не означает полного исключения доступа к обычному указателю. Иногда такой доступ просто необходим. Следовательно, все интеллектуальные указатели должны обеспечивать явмый доступ к скрытому внутри них обычному указателю с помошью вызова функции. чозд Рцп(5овеспдод* р) 5вагспсг<5овесЫпд> зр; Гцп(цетсвр1(зр)); // явное преобразование всегда разрешено Вопрос заключается не в том, можете ли вы получить доступ к обычному указателю, а в том, насколько это легко. Может показаться, что разница между явным и неявным преобразованиями невелика, однако она очень важна.
Неявное преобразование происходит независимо от программиста, он может даже ничего не знать о нем. Явное преобразование, как, например, вызов функции бетсвр1, выполняется осознанно, управляется программистом и не скрывается от пользователей программы. Неявное преобразование интеллектуального указателя в простой указатель желательно, но иногда опасно. Класс 5вагсРсг предоставляет программисту право самому решать, допускать его или нет. По умолчанию предлагается безопасный вариант— неявное преобразование запрешается. Явный доступ к обычному указателю всегла открыт через функцию пес1вр1.
7.8. Равенство и неравенство Любой трюк в языке С++, например, описанный выше (внутренняя неоднозначность), создает новый контекст, который в свою очередь может иметь неожиданные последствия. Рассмотрим проверку интеллектуальных указателей на равенство и неравенство. Интеллектуальный указатель должен поддерживать такой же синтаксис проверки, что и обычные указатели. Программист ожидает, что приведенные ниже проверки будут скомпилированы и выполнены. 5вагСРСг<5оветпзпд> зр1, зр2; 5овеспзпд* р; з Р (зр1) // проверка 1: прямая проверка ненулевого указателя 7пвва 7.
Интеллектуальные указатели 41 (!зрй) // проверка 2: прямая проверка нулевого указателя )т (зрХ == О) // проверка 3: явная проверка нулевого указателя 5 б (зр1 == зр2) // проверка 4: сравнение двух интеллектуальных указателей 5б (зр1 == р) // проверка 5: сравнение с обычным указателем В этом фрагменте проиллюстрированы не все возможные проверки. Реализовав проверку равенства, мы сможем легко проверять и неравенства.
К сожалению, между проблемой проверки равенства и проблемой предотвращения компиляции оператора ое1ете существует скрытая связь. Если в программе определено только одно преобразование, большинство проверок (за исключением проверки 4) проходит успешно, и программа выполняется в соответствии с ожиданиями программиста. Единственный недостаток — возможность непредвиденного применения оператора ое1ете к интеллектуальному указателю. Если в программе определены два преобразования (внутренняя неоднозначность), неверные вызовы оператора де1ете распознаются, но ни одна из указанных выше проверок больше не компилируется— они также становятся неоднозначными. Дополнительное преобразование интеллектуального указателя в булевскую переменную является полезным, но небезопасным.
Рассмотрим следуюшее определение интеллектуального указателя. тевр1ате <с1аьа т> с1азв 5вагтясг ( рцЬ15 с: орегатог Ьоо10 сопят ( гетмгп робптее != 0; Четыре указанные выше проверки успешно компилируются, но при этом выполняются следуюшие бессмысленные операции. 5юагтятг<дрр1е> зр1; 5юагтятг<огапое> зр2; // Яблоко и апельсин — не одно и то же бт (зр1 == зр2) // оба указателя преобразовываются // в булевскую переменную, // а результаты сравниваются 1т (Бр1 != 5р2) // то же самое Ьоо1 Ь = зр1; Н (зр1 * 5 == 200) // Допустимая операция // Ой! Интеллектуальный указатель // ведет себя как целое число! 196 Часть !!. Компоненты Как видим, мы можем получить либо все, либо ничего. Добавив преобразование интеллектуального указателя в булевскую переменную, мы допустили ситуации, когда объект класса 5юагтятг ведет себя неправильно. Таким образом, определение оператора Ьоо1 для интеллектуального указателя оказалось неразумным решением.
Истинное, всеобъемлюшее и надежное решение этой дилеммы заключается в полной перегрузке всех операторов по отдельности. При таком подходе все операции, имеюшие смысл для обычных указателей, окажутся применимыми и для интеллектуальных указателей без каких-либо побочных эффектов. Вот как можно реализовать эту илею. сеер»асе <с1ааа т> с1аьв 5иагСРСг ( рцЬ)зс: Ьоо1 орегасог! О сопэс // допускает оператор "ИФ (!эр) ( гесвгп ро1псее 0; ) (п!(пе Ггзепд Ьоо1 орегасог==(сопэс 5иагсрсгб 1Ьз, сопэс т+ гЬБ) ( гесигп 1Ьэ.ро»псее == гйз," »п1(пе тг(епд Ьоо1 орегасог==(сопзс т* 1Ьэ, сопэс 5еагсясг" гйэ) ( гесигп 1бв == гбв.ро1псее ; ) »п1(пе Угзепд Ьоо1 орегасог!=(сопэс 5иагсрсгб 1Ьэ, <опас т* гЬ5) гесцгп 1Ьз.розпсее != гйэ; ) 1п1(пе тг(епд Ьоо1 орегасогб>(сопзс т* 1Ьэ, сопят 5еагСРСгй гйв) гесцгп 1бв ! гйз.ро(псее Да, это неприятно, однако этот подход решает проблемы, касающиеся практически всех сравнений, включая сравнение с литеральным нулем.
Операторы пересылки, содержашиеся в этом фрагменте кода, передают операторы, которые код пользователя применяет к интеллектуальным указателям, обычным указателям, скрытым внутри. Это самое реалистичное решение. Однако проблема решена не полностью, Если в программе предусматривается автоматическое преобразование указателей, остается риск„связанный с неолнозначностью такой операции. Допустим, у нас есть класс вазе и класс оег1чед, производный от него. Тогда в следующем коде будет содержаться неоднозначность. 5еагсисг<ваэе> эр; оегзчед* 'р; зг (зр == р) (» // ошибка! Существует неоднозначность между // проверкой '(вазе*)зр (вазе )р' и // функцией 'орегасог==(зр, (вазе )р) Действительно, разработка интеллектуальных указателей — занятие не для слабонервных. Но и это еше не все.
Кроме определения операторов == и ! =, мы можем создать их шаблонные версии. севр1асе <с1азз т> с1аав 5щагсрсг 197 Глава 7. Интеллектуальные указатели ( риЬ)1с: как и раньше севр)асе <с1а55 Ц> 1п15пе тгз'епд Ьоо1 орегасог==(сопзС 5вагСРСгб 1Ь5, соп5с ц* гЬ5) гесвгп 1Ьз.ро1псее == гЬ5; севр)асе <с1а55 ц> )п1тпе тгз'епд Ьоо) орегасог==(сопзс ц* 1Ь5, сопзс 5вагсрсгб гЬ5) ( гесцгп 1Ь5 == гйв.ро)псее аналогично определенный оператор != ...
Шабдонные операторы выполняют сравнения с любым типом указателей, устраняя неоднозначность. Если все так хорошо, зачем сохранять обычные, не шаблонные операторы, работавшие с типами объектов, на которые ссылаются указатели? Эти операторы никогда не пройдут процедуру сопоставления, потому что шаблоны сопоставляют любые типы указателей, в том числе и типы объектов, на которые ссылаются указатели. Однако "никогда не говори никогда".
При выполнении проверки )б (зр == 0) компилятор пытается выполнить следуюшие сопоставления. ° шаблонные операторы (~егпр)азед орегазогз). Они не проходят проверку, поскольку нуль не является типом указателей. Литеральный нуль можно неявно преобразовать в тип указателей, однако шаблонное сопоставление не предусматривает преобразований типов. ° Нешаблонные операторы (попзепзр)азед орега[огз). Исключив шаблонные операторы, компилятор переходит к проверке нешаблонных.
Один из этих операторов оказывается подходяшим после выполнения неявного преобразования литерального нуля в тип указателя. Если бы нешаблонных операторов не было, проверка завершилась бы сообщением' об ошибке. Итак, и шаблонные, и нешаблонные операторы одинаково необходимы. Посмотрим теперь, что праизойлет при попытке сравнения двух классов 5вагСРСг„ конкретизированных разными типами.
5вагсРСг<лрр1е> зр1; 5вагСРСг<агапде> зр2; зт (5р1 == зр2) Компилятор споткнется на этом сравнении из-за неоднозначности: в каждой из этих двух конкретизаций определен оператор ==, н компилятор не знает„который из них выбрать. Мы мажем избежать этой проблемы, определив '"ликвидатор неоднозначностей", севр1асе <с1а55 т> с1а55 5вагсрсг рцЬ)сс: // Ликвидатор неоднозначностей севр)асе <с1авв ц> Ьоо1 орегасог= (сопэС 5вагСРСг<Ц>6 гйв) Салат 198 Часть П.