Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 67
Текст из файла (страница 67)
*! В данном случае объект класса йзвр1ауей сая)«имеет два подобъекта класса 11п)с савх г г1)п)с и й1яр1ауей г: 11пм Часто это бывает полезно, например в случае реапизации списков, когда требуется, чтобы каждый эпемент списка содержап поле связи. В результате обьект класса азяр1ауед сая)г находится одновременно в списках объектов йзяр1ауей и тая)с". Это можно представить и графически, показав, из каких подобъектов состоит йзяр1ауей саяй (см. рис. 12.1).
11пк 11п)г каях дгяр1ауед д1вр1ауед Сая Рис. 12.1 «Я называю это независимым множественным наследованием. Но во многих случаях применения множественного наследования предпопагается наличие зависимости между базовыми кпассами [например, предоставпение каких-то возможностей дпя окна). Такие зависимости можно выразить с помощью объекта, раздепяемого разпичными производными кпассами.
Другими словами, требуется указать, что базовый класс должен привносить лишь один объект в конечный Я не считаю такой стиль представления списка идеальным во всех ситуациях, но иногда он оптимален. Поэтому С++ поддерживает приведенный вьппе пример. По умолчанию базовый класс, появляющийся в иерархии наследования дважды, будет представлен двумя подобьектами.
Однако возможно и другое разрешение ситуации [8ггопяггпр, 1987): Множественное наследование ИИИИИИИВ производный класс, даже если он встречается в иерархии наследования несколько раз. Чтобы отличить этот случай от независимога множественного наследования, такие базовые классы описываются как виртуальные: с1авв Анг рцЫдс н(ггпа1 Х ( /* ... */ ); с1авв ВИ: рпЫ1с н(гвпа1 и ( /* ... */ с1авв Сиг риЫдс АИ, риЫ1с ВИ ( /* ...
*/ ); Единственный объект класса и будет разделяться на АИ и ВИ; иными Слсвами, в СИ должен быть включен только один объект и, хотя си наследует и Аэ), и Ви. Если не очи~ать того, что в производном классе возникает всего адин объект, виртуальный базовый класс ведет себя точно так же, как обыкновенный. Виртуальность и- это свойство наследования, которое задается дпя Аи и Ви, а не для самого и. Все виртуальные базовые классы в графе наследования относятся к одному и тому же объектул. Графически зто представлено на рис. 12.2 Г~ '" Г Рис. 12.2 Какие классы могут выступать в роли у(, Ау(, Ву( и Су( в реальных программах? Первоначальным примером была простая оконная система, основанная на идеях, почерпнутых из литературы по 1.(зр, см.
рис. 12.3. Г'. и).пдои и1ЪЬ лепя и(пдои и(сЬ Ъакдет ' Г идпдои и1сЬ птенец апд Ъатдек Рис. 12.З На мой взгляд, этот пример искусственный, но он взят из реальной программы и, что особенно важно для представления, интуитивно понятен. Несколько примеров можно найти и в стандартной библиотеке потокового ввода/вывода ('Ягор(го, 19891 (см. рис. 12А). Виртуальные базовые классы ЯИИИИИИИ Г" ьвпгеапп овггеапп говпгеае ГГвпгеае оевггеае Гвпгеаж Рис. 12.4 12.3Л. Виртуальные базовые классы и виртуальные функции Сочетание абстрактных и виртуальных базовых классов предназначалось для поддержки стиля программирования, который был бы приблизительно эквивалентен смешанному стилю (ш1х)п згу! е) в некоторых системах на 1.1зр.
Это означаетг, что абстрактный класс определяет интерфейс, а реализацию предоставляет ряд производных классов. Каждый производный класс (компонент — ппхш) делает вклад в полный класс (смесь — щ1х). Мне неизвестно, почему виртуальные базовые классы следует считать более полезными или фундаментальными, чем обыкновенные, и наоборот.
Поэтому С+~- поддерживает те и другис. По умолчанию решено было принять обыкновенный базовый класс, так как на его реализацию, в отличие от виртуального, тратится меньше времени и памяти; к тому же «программирование с помощью виртуальных базовых классов несколько сложнее, чем с помощью обыкновенных. Проблема в том, чтобы избежать множественных вызовов функции из виртуального класса, когда это нежелательно» [2по1, см. также раздел 12.5. В связи с трудностями реализации возникло искушение не включать в язык понятие виртуального базового класса.
Однако я счел решающим довод о необходимости какого-то способа представления зависимостей между потомками одних родителей («братьев»). Классы-«братья» могут взаимодействовать между собой только через общий корневой класс, через глобальные данные или через явные указатели. Если бы не было виртуальных базовых классов, то необходимость существования общего корня привела бы к чрезмерно широкому использованию «универсальных» базовых классов. Смешанный стиль, описанный в разделе 12.3.1, дает пример такою взаимодействия «братьев».
Если уж поддерживать множественное наследование, то подобный механизм необходим. С другой стороны, наиболее полезным я считаю простое применение множественного наследования вроде определения класса, который обладает суммой атрибутов двух других независимых классов. Множественное наследование ПИИИИИИФ! Чтобы поддержать смешанный стиль, необходимы два условия: о возможность замещать виртуальные функции базового класса из двух различных производных классов; в противном случае существенныс части реализации должны наследоваться только по одной цепочке, как в примере в11вс вес из раздела 13.2.2; о возможность определить, какая функция замещает виртуальную, и перехватить некорректные замещения; иначе пришлось бы полагаться иа зависимость от порядка или разрешение во время вьшолнения. Рассмотрим приведенный выше пример, Предположим, что в классе Х есть виртуальные функции й ( ) и д ( ): с1авв и ( // чьгеаа1 чо1г) Г(); чггсса) чо1г) д(); а в каждом из Ах и ВХ замещающие одну из них: с1авв йх : риЫьс ч(ггаа1 Х ( // чоьо д(]; ): с1авв Ви: риЫгс ч1ггыа1 и ( // чо1г) '(); с1авв си: риыгс яи, риы1с вх, риыьс ч1ггва1 и ( // ); Тогда СХ можно использовать так: Си* рсы = геи СИ; Аи* раы = рси; ВИ" рЬы = рси; чо1й ГВ"() ( рси->1(); // вызывается ВИ:;Й() рси->д(); // вызывается Ах::д() раы->1(); // вызывается Вх;:Г()1 рЬы->Г(); // вызывается Ах:са()! ) Как обычно, одна и та же виртуальная функция вызывается независимо от типа указателя на объект.
Именно это и позволяет различным классам добавлять 16ИИИИИИИ Виртуальные базовые классы с1авв Ч ( рцЫьс: ьпс т() г ьпс х; ) г с1авв В: рцЫьс ч1гаца1 Ч ( рцЫьс: 1пс т()," тпс х; С1авв С: рцЫ1С ч1гсаа1 Ч ( ); с1авв В: рцЫтс В, рцЫтс С ( чоьа д() г чоьа пг гд() хггг г" () г // правильно: Вг:х доминирует над Чггх // правильно: Вггт() доминирует над Чггт() (рафически это выглядит так, как показана на рис. ) 2.5. Рис.
(2.5 Отметим, что доминирование определяется по отношению к любым именам, о не только к функциям. Правила доминирования необходимо дпя применения виртуальных функций, ибо именно ано определяет, кокая функция будет исполняться при виртуальном вызове. Опыт показывает, что оно столь же хорошо подходит и дпя невиртуапьных функций. При использовании ранних версий компилятора, где правило доминирования к невиртуапьным функциям не применялось, возникали ошибки, и приходилось писать плохо структурированные программыь.
Для разработчика компияятора правило доминирования — обычное правило поиска имени, применяемое для того, чтобы понять, существует ли уникальная функциональность к общему базовому классу и пользоваться тем, что добавил другой класс. Естественно, при проектировании производных классов зто следует иметь в виду, а иногда необходимо кое-что знать о классах, наследующих из тех же базовых. Чтобы замещение виртуальных функций вдоль различных ветвей было возможным, необходимо выработать правило о том, какие сочетания замещаю(цих функций допустимы, а какие должны считаться ошибкой.
При обращении к виртуальной функции нужно, чтобы вызывалась одна и та же реальная функция независимо от того, как задан объект класса. Мы с Эндрю Кенигом сформулировали единственное, на наш взгляд, правило, которое гарантирует такое поведение: «имя В г г т доминирует над именем А: г г, если его класс А является дпя класса В базовым. Если одно имя имеет более высокий приоритет по сравнению с другим, то при выборе между ними не возникает неоднозначности; используется доминирующее имя. Например: Множественное наследование ИИИИИИИИ11 функция, которую можно поместить в таблицу виртуальных функций. Более слабое правило не гарантировало бы этого, а более сильное привело бы к запрету осмысленных вызовов. Правила для абстрактных классов и правило доминирования гарантируют, что объекты можно создавать только для таких классов, которые предоставляют полный и непротиворечивый набор сервисов.
Без этих правил при работе над нетривиальными проектами у программиста было бы мало надежд избежать серьезных ошибок во время выполнения. 12.4. Модель размещения объекта в памяти Множественное наследование усложняет модель объекта в двух отношениях: сз у объекта может быть несколько таблиц виртуальных функций; щ при просмотре таблицы виртуальных функций должен существовать способ найти подобъект, соответствующий классу, из которого взята замещаюшая функция. Рассмотрим пример: с1авв А ( риЫ(с: ч1гсиа1 чосе ((1пс); ); с1авв В ( риЫ(с: чггсиа1 чо(о 1((пс); ч1гсиа1 чо(й д(); ): с1авв С: риЫ(с А, риЫ1с В ( риЫ(с: чо1о Е(ьпс); ): Объект класса С мог бы выглядеть в памяти, как показано на рис.
) 2.6. Две таблицы виртуальных функций чс ь1 необходимы потому, что в программе могут быть объекты классов д и В наряду с объектами класса с, в которых есть части, соответствующие классам А и В. Получая указатель на В, надо уметь вызывать виртуальную функцию, не зная, что такое  — <настоящий объект класса В», вчасть В, принадлежащая объекту класса С» или еще какой-то объект, включающий в. Поэтому у каждого в должна быть своя чсЬ1, доступ к которой осуществляется одинаково во всех случаях.