В ромбовидной иерархии наследования зачем базовый класс делают виртуальным?
Виртуальное наследование гарантирует, что в наиболее производном объекте будет ровно один подобъект общего базового класса, даже если к нему ведут несколько ветвей наследования. Это устраняет дублирование состояния и неоднозначность обращения к членам общего предка.
Множественное наследование позволяет классу получать интерфейс или реализацию от нескольких родителей. При обычном наследовании две ветви ромба независимо включают базовый класс, поэтому производный объект содержит две его копии.
Виртуальное наследование появилось как механизм явного описания общего предка в такой иерархии. Оно решает проблему дублирования базовой части, но усложняет правила конструирования и обычно делает объект менее простым по внутреннему представлению.
Пусть два класса наследуются от одного базового, а третий класс наследуется от обоих промежуточных. При обычном наследовании третий класс содержит два подобъекта общего базового класса.
Это приводит к нескольким последствиям: поле общего предка хранится дважды, обращение к нему из наиболее производного класса может быть неоднозначным, а изменение одной копии не меняет вторую. Простое приведение к базовому типу также требует указать, по какой ветви выполняется преобразование.
При виртуальном наследовании промежуточные классы содержат не самостоятельные копии общего базового класса, а сведения, позволяющие найти единый виртуальный базовый подобъект. Этот подобъект создаётся внутри наиболее производного объекта только один раз.
В примере Most содержит одну общую часть Base, поэтому обращение к object.value однозначно, а результатом будет 42.
Ключевое правило конструирования состоит в том, что виртуальный базовый класс инициализирует наиболее производный класс. Конструкторы Left и Right не создают отдельные экземпляры Base; если Most не укажет инициализацию Base, будет использован конструктор Base по умолчанию, если он доступен.
Виртуальное наследование не означает виртуальность всех методов и не связано с виртуальными функциями. Оно также не устраняет любую неоднозначность: одинаково названные члены, объявленные непосредственно в разных промежуточных классах, по-прежнему могут требовать явного выбора ветви.
Цена механизма — более сложная компоновка объекта, дополнительные служебные данные или косвенность при доступе к виртуальной базовой части, а также более сложные правила конструирования и приведения типов. Поэтому виртуальное наследование применяют, когда действительно требуется единый общий базовый подобъект, а не просто для устранения любой сложности множественного наследования.
В библиотеке есть общий класс Entity с идентификатором. Классы Renderable и Serializable получают его интерфейс, а Document наследуется от обоих. При обычном наследовании у Document появляются два идентификатора Entity, что делает API неоднозначным и допускает рассинхронизацию значений.
Вариант с отказом от множественного наследования и композицией сделал бы владение состоянием очевиднее и упростил компоновку объекта, но потребовал бы отдельной передачи общего интерфейса через поля или делегирование. Вариант с обычным множественным наследованием проще реализуется, однако сохраняет две независимые базовые части.
Выбранное виртуальное наследование оправдано, если Renderable и Serializable действительно являются разновидностями одной сущности и должны обращаться к одному идентификатору. Общий объект Entity создаётся конструктором Document, а проблема дублирования исчезает. Если же промежуточные классы лишь используют сервис без отношения «является», предпочтительнее композиция.
Кто инициализирует виртуальный базовый класс?
Это делает наиболее производный класс. Инициализаторы Base в конструкторах промежуточных классов учитываются только при создании объекта самого промежуточного класса; при создании более производного объекта они не определяют состояние виртуальной базовой части. Если наиболее производный класс не инициализировал Base, вызывается его конструктор по умолчанию.
Чем виртуальное наследование отличается от виртуальных функций?
Это независимые механизмы. Виртуальная функция выбирается динамически по фактическому типу объекта при вызове через подходящий базовый интерфейс. Виртуальное наследование определяет количество общих базовых подобъектов и способ их размещения в иерархии; оно само по себе не включает динамический полиморфизм методов.
Всегда ли виртуальное наследование устраняет неоднозначность приведения к базовому классу?
Оно устраняет неоднозначность, возникающую из-за двух копий одного и того же виртуального базового класса: преобразование к нему ведёт к единому подобъекту. Но если в разных ветвях объявлены разные одноимённые члены или функции, выбор имени может оставаться неоднозначным. В таком случае требуется квалификация имени, переопределение интерфейса или явное объявление нужного члена в наиболее производном классе.