Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 63
Текст из файла (страница 63)
Управление наотедованием Закрытый деструктор предотвращает также и наследование. Например: с1аяя О: рпЫгс Оп атее ясоге ( // ): Р о; // ошибка; нельзя вызвать закрытый деструктор базового класса Это позволяет классу с закрытым деструктором стать логическим дополнением абстрактного класса. Наследовать классу Оп ггее всоге невозможно, поэтому ЕИИИ>ИИИ Перегрузка с1авв ПваЬ1е 1осх ггтепд цваЫе; рг1часе: цваЫе 1оск() () с1авв цваь1е: рпытс чтгсиа1 пваые 1оск ( // роЫьс: УваЬ1е()/ ПваЫе(сьаг*); // ); ПваЫе а; с1авв 00 : риЬььс ЦваЬ1е ( ); РР с)б; // ошибка: РР:пао() недоступен // цваЫе 1осх: пдваЫе 1оск() — закрытый член Этот пример основан на правиле, что произволный класс может вызывать конструктор виртуального базового класса (явно или неявно).
Однако такие примеры — скорее, предмет лля интеллектуальных дебатов, чем для применения на практике. 11.4.4. Лочленное копирование Первоначально присваивание и инициализация были по умолчанию определены как побитовое копирование. Это приводило к неприятностям, когда объект класса, в котором определен оператор присваивания, использовался в качестве члена класса, в котором такой оператор не был определен: с1авв х ( /* ... */ хв орегасог=(сопев ха); )~ всгпсс х ( х а; ); чо1с) Г(у у1, Х у2) ( у1 = у2; ) Здесь у2. а копируется в у1. а цобитово. Очевилно, что зто неправильно н является результатом недосмотра при проектировании оператора присваивания и копируюшего конструктора. После ряла споров и упреков со стороны Эндрю при вызовах виртуальных функций данного класса необязательно использовать механизм виртуализации.
Однако я не лумаю, что какой-нибудь из современных компиляторов выполняет такую оптимизацию. Впослелствии Энлрю Кениг обнаружил, что можно запретить наслелование даже вне зависимости от того, в какой памяти может размешаться объект: Создание и копирование объектов НИИИ ИИИИ Кенига было принято простое решение: копирование объектов определено как почленное копирование не-статических членов и объектов базовых классов. Согласно этому определению, х=у означает то жс, что х.
орегасог= (у) . Но здесь есть одно интересное (хотя и не всегда желательное) следствие. Рассмотрим пример: ставя Х ( /* ... */ ставя у : рцЫЬс х ( /* . . */ ); уоьд д(Х х, у у) ( х = у; // х.орекасот=(у): правильно у = х; // у.орегасог=(х): ожибка х — нв объект класса У ) с1авв Бсквпд ( сьаг *р; Ьпг. вж; рцыьс: // здесь не определены операции копирования (небрежность) ); уоЫ, Е(солне Згкьлча в) ( Ясгьлд в2 = я/ в2 = в/ ) // предупреждение: копируется указатель // предупреждение: копируется указатель Семантику присваивания и копирования по умолчанию можно назвать поверхностным копированием.
Иными словами, копируются члены класса, но не объекты, на которые эти члены указывают. Альтернативу — рекурсивное копирование По умолчанию присваивание объекту класса х — это х:: орегагок= (сопвг хй ), поэтому х=у — законная операция, ибо у открыто наследует х. Обычно такую операцию называют вырезанием (в)1с(пц), так как х присваивается «срез» у, Копирующие конструкторы трактуются аналогично. С практической точки зрения вырезание представляется мпе сомнительным трюком, но я не вижу способа запретить его, не вводя слишком уж специальных правил. Кроме того, в то время Рави Сетхи как раз просил меня ввести именно такую «семантику вырезания», она была нужна ему для теоретических и педагогических целей: если це разрешить присваиванис объекта производного класса объекту его открытого базового класса, то это будет единственным местом в С++, где производный объект нельзя использовать вместо базового.
Но при этом остается открытой проблема, связанная с операциями копирования по умолчанию: указатели-члены копируются, а объекты указания — нет. Это почти всегда неправильно, но запретить операцию нельзя из-за совместимости с С. Однако компилятор легко может выдать предупреждение, если класс с указателем-членом инициализируется при помокни копирующего конструктора или оператора присваивания по умолчанию. Например: ИИИИ>ИП1 Перегрузка указываемых объектов (так называемое глубокое копирование) — следует определять явно. Поскольку объекты могут указывать сами на себя, то вряд ли возможно другое решение.
В обычном случае не стоит пытаться определять присваивание как глубокое копирование; гораздо лучше определить виртуальную (или невиртуальную) функцию копирования (см. [2п((, стр. 217 — 220) и раздел 13.7). 11.5. Удобство нотации Я хотел позволить пользователю самому опрелелять смысл каждого оператора, если только это разумно и не вступает в серьезное противоречие с предопределенной семантикой.
Было бы прогце разрешить перегрузку всех без исключения операторов либо запретить перегрузку любого оператора, имеющего предопределенную семантику для объектов класса. Принятое в результате компромиссное решение устраивает ие всех. Почти все споры и подавляющее большинство сложностей, с которыми мы столкнулись, относятся к операторам, которые не укладываются в привычную схему бинарных или префиксных арифметических операторов. зсгисс х ( тпс ьо с1аев Рсг ( т* р ° !! ривттс: Рсг(вупьоттс гег); -Рсг; т* орегасог->() ( /! проверить р тесигп р; 11.5.1.
кУмные» указатели До версии 2.0 пользователи не могли псреопределять оператор разымеиования ->. Это осложняло создание классов объектов, ведущих себя как «умные» указатели. При опредечснии перегрузки операторов -> виделся мне как бинарный оператор со специальными правилами для правого операнда (имени члена). В атой связи вспоминается встреча в компании Мептог Сгарп(сз в Орегоне, когда. Джим Ховард () (ш Нотчагг)) доказал мне, что я заблуждался. Оператор ->, объяснил он, можно рассматривать как унарный постфиксный оператор, результат которого применяется к имени члена. Пересматривая механизм перегрузки, я воспользовался атой идеей. Если тип значения, возвращаемого функцией орегагот-> ( ), используется, то он должен быть указателем на класс или объект класса, в котором определен оретасот-> () . Например: МИИИИИИП Удобство нотации Здесь класс Рот определен так, что его объекты ведут себя как указатели на объекты класса у с тем отличием, что при каждом обращении производятся некоторые вычисления: чо1б 1(Ртг х, Ртта хт, Ртг* хр) ( х->в; !! х.оретасот->()->в; то есть х.р->в хт->в; !! хт.орега ог->()->в; то есть хг.р->в хр->в; /! ошибка: у Ртт иет члена в Такие классы особенно полезны в виде шаблонов (с>ь раздел 15.9.1), [2п<Ц: гевр1асе<с1авв у> с1авв Ргг ( /* ...
*! ); чоха 1(рст<совр1ех> рс, Ргт<з)ларе> рв) ( !* ... *! ) Это было понятно уже после реализации перегрузки -> в 1986 г. К сожалению, написать такой код удалось лишь после реализации шаблонов. Для обычных указателей -> — это синоним некоторых применений * и [ ]. Папример, для объявления у* р имеют место тождества: р->в == ( р) .в == .р[0) .в Как обычно, для определенных пользователем операторов таких гарантий не дается. При желании эквивалентность можно обеспсчитгс с1авв Ргт ( ~* р рпЬ11с: у* орегагог->() ( геситп р; ) Ха оретатот*() ( тетпгп *р; ) уа орегагот()(1пт т) [ тегпгп р(т); !! ); Перегрузка оператора -> важна для целого класса программ.
Причина в том, что косвенное обращение — ключевая концепция, а перегрузка -> дает красивый, прямой и эффективный способ реализации ее в программах. Еше одно применение оператора -> — это ограниченная, но очень полезная форма реализации делегирования в С++ (см. раздел 12.7). 1ь5.2. «Умныен ссылки Решив разрешить перегрузку оператора ->, я залумался над тем, можно ли аналогичным образом перегрузить оператор . (точка). В то время мне казались убедительными следующие рассуждения: если оЬ1— зто объект класса, то о)з) . в имеет смысл для каждого члена в этого объекта.
Персопределяя встроенные операции, мы не хотим получить мутируюший язык (хотя по очень веским причинам это правило нарушено для =, а также для унарного а). ИИИИИИИИВ Перегрузка с1аяя Нци ( // рцЬ11с: нпиа оретасог=(сопят Мциа); тпс оретасот[](1пс); // получить одну цифру Нци оретасог+(сопвс нциа); чо1б стцпсасенб1д1св(1пс)/ // отбросить Я надеялся определить класс неЖцпс который ведет себя как ицма, но выполняет некоторые дополнительные действия.
Например, если я могу написать чо1б 1(ици а, Нци Ь, Нци с, тпС 1) ( // с =аЬ/ 1пс с)1д1с = с(].); с.сгцпсасенб1д1св(1); // то хотелось иметь возможность написать и такое: чо1с) д(негипи а, аесици Ь, Ве1ици с, тпС 1) ( // с = а+Ь; 1пс б1д1св = с[1]; с.сгппсасенс)1д1св(1); // ) Допустим, что орегас ос .
( ) определен полностью аналогично орегасог-> ( ). Сначала попробуем очевидное определение нес)чцлс с1авя аегици ( Нссп *р; рцЬ11с: аеЫци(нций а) ( р = аа; Если разрешить псрегрузку . для класса х, то не удастся обычным способом обратиться к его членам; придется использовать указатель и оператор ->, хотя -> и а тоже могут быть переопределены. Эти аргументы являются весомыми, но нс решающими. В частности, в 1990 г. Джим Эдкок (]]ш А(]сос]с) предложил разрсшить перегрузку оператора . точно так же, как оператора ->.
Зачем может понадобиться перегрузка орегасог. ( )? Чтобы создать класс, работающий как «описатель» (пап([!е) или «заместитель» (ргоху) другого класса, в котором и выполняются основные действия. В качестве примера, приводившегося во время первых обсуждений перегрузки орегасог. (), рассмотрим класс целых чисел повышенной разрядности: Удобство нотации ИИИИИИИП Ицшй орегасог. () ( бо вошесЫпд(р); гесцгп *р; ) ного Ыпб(нцшй д] ( Р = йсн ) К сожалению, это не дает нужного результата, так как . не указана явно во всех случаях: с = аеЬ; // точки нет гпг бтдйгв = с[й); // точки нет с,сгцпсасенбгдгсв(й); // вызов орегасог.() Придется написать функции-переходники, гарантирующие, что прн применении операторов к Кейнцп] выполняются правильные действия: с1авв Кегнцш Ицш *Р; рцЫйс: Кегнцт(нпшй а) ( р = йа; ) Иптй орегагог.(] ( бо вошеСЫпд(Р]; гегцгп *Р; ) ногб Ыпб(нашй д) ( Р = йд] ) // функции-переходники кейицшй орегагог=(сопвс Кегнпшй а) бо вошеСЫпд(р); *р=*а.р; гегцгп *СЫв; ) йпс орегасог[)(тпс й) ( бо вошесЫпд(р); гесцгп (*р)[й); Кепками орегасоге(сопвс петиций а) ( бо вошесЫпд(р)] гесцгп кегицш(*р+*а.р); ) нойб Ь(аегнцш г, Ицшй х] ( г.Ыпб(х); (йг)->Ыпб(х); ) // ошибка: Ицш::Ьгпб отсутствует // правильно: вызывается Кесипш::Ьйпб Пользователи С++ разошлись во мнениях относительно того, какая интерпретация орегас.ог.
( ) лучше. Я склоняюсь к тому, что если уж разрешать перегрузку орегасог. ( ), то он должен вызываться как для явных, так и для неявных операций. В конце концов, определяется он для того, чтобы не писать функции-переходники. Если неявные использования . не интерпретируются с помощью орегасог. ( ), то по-прежнему приходится писать множество переходников либо не перегружать этот оператор вообше.