Множественное наследование и чистые виртуальные функции
Следующий код:
struct interface_base
{
virtual void foo() = 0;
};
struct interface : public interface_base
{
virtual void bar() = 0;
};
struct implementation_base : public interface_base
{
void foo();
};
struct implementation : public implementation_base, public interface
{
void bar();
};
int main()
{
implementation x;
}
не удается выполнить компиляцию со следующими ошибками:
test.cpp: In function 'int main()':
test.cpp:23:20: error: cannot declare variable 'x' to be of abstract type 'implementation'
test.cpp:16:8: note: because the following virtual functions are pure within 'implementation':
test.cpp:3:18: note: virtual void interface_base::foo()
Я общался с ним и понял, что создание наследования "interface → interface_base" и "implementation_base → interface_base" виртуально, устраняет проблему, но я не понимаю, почему. Может кто-нибудь объяснить, что происходит?
p.s. Я пропустил виртуальные деструкторы с целью сделать код короче. Пожалуйста, не говорите мне, чтобы я их ввел, я уже знаю:)
Ответы
Ответ 1
У вас есть два базовых класса interface_base
в дереве наследования. Это означает, что вы должны предоставить две реализации foo()
. И вызов любого из них будет действительно неудобным, требующим многократных бросков, чтобы устранить неоднозначность. Обычно это не то, что вы хотите.
Чтобы решить эту проблему, используйте виртуальное наследование:
struct interface_base
{
virtual void foo() = 0;
};
struct interface : virtual public interface_base
{
virtual void bar() = 0;
};
struct implementation_base : virtual public interface_base
{
void foo();
};
struct implementation : public implementation_base, virtual public interface
{
void bar();
};
int main()
{
implementation x;
}
При виртуальном наследовании в наследственной иерархии для всех виртуальных упоминаний создается только один экземпляр базового класса. Таким образом, существует только один foo()
, который может выполняться implementation_base::foo()
.
Для получения дополнительной информации см. этот предыдущий вопрос - ответы содержат некоторые хорошие диаграммы, чтобы сделать это более понятным.
Ответ 2
обычный С++ idiom:
- публичное виртуальное наследование для классов
- приватное не виртуальное наследование для классов
В этом случае мы имели бы:
struct interface_base
{
virtual void foo() = 0;
};
struct interface : virtual public interface_base
{
virtual void bar() = 0;
};
struct implementation_base : virtual public interface_base
{
void foo();
};
struct implementation : private implementation_base,
virtual public interface
{
void bar();
};
В implementation
уникальная виртуальная база interface_base
:
- публично наследуется через
interface
: implementation
--public → interface
--public → interface_base
- конфиденциально унаследован через
implementation_base
: implementation
--private → implementation_base
--public → interface_base
Когда код клиента делает одно из этих преобразований для базовых преобразований:
- полученный для преобразований базового указателя,
- ссылка привязки базового типа с инициализатором производного статического типа,
- доступ к унаследованным элементам базового класса через lvalue производного статического типа,
важно только то, что существует хотя бы один доступный путь наследования от производного класса к подобъекту базового класса; другие недоступные пути просто игнорируются. Поскольку наследование базового класса является только виртуальным, существует только один объект базового класса, поэтому эти преобразования никогда не являются двусмысленными.
Здесь преобразование из implementation
в interface_base
всегда может выполняться клиентским кодом через interface
; другой недоступный путь вообще не имеет значения. Уникальная виртуальная база interface_base
публично унаследована от implementation
.
Во многих случаях классы реализации (implementation
, implementation_base
) будут скрыты от клиентского кода: будут показаны только указатели или ссылки на интерфейсные классы (interface
, interface_base
).
Ответ 3
В случае "решения" проблемы наследования алмазов решения, предлагаемые bdonlan, действительны. Сказав это, вы можете избежать проблем с бриллиантами с дизайном. Почему каждый экземпляр данного класса рассматривается как оба класса? Вы собираетесь передать этот же объект классу, который говорит что-то вроде:
void ConsumeFood(Food *food);
void ConsumeDrink(Drink *drink);
class NutritionalConsumable {
float calories() = 0;
float GetNutritionalValue(NUTRITION_ID nutrition) = 0;
};
class Drink : public NutritionalConsumable {
void Sip() = 0;
};
class Food : public NutritionalConsumable {
void Chew() = 0;
};
class Icecream : public Drink, virtual public Food {};
void ConsumeNutrition(NutritionalConsumable *consumable) {
ConsumeFood(dynamic_cast<Food*>(food));
ConsumeDrink(dynamic_cast<Drink*>(drink));
}
// Or moreso
void ConsumeIcecream(Icecream *icecream) {
ConsumeDrink(icecream);
ConsumeFood(icecream);
}
Конечно, было бы лучше в этом случае для Icecream
просто реализовать NutritionalConsumable
и предоставить метод GetAsDrink()
и GetAsFood()
, который вернет прокси-сервер, исключительно для того, чтобы появляться как пища или напиток. В противном случае это предполагает, что существует метод или объект, который принимает Food
, но каким-то образом хочет увидеть его как Drink
, который может быть достигнут только с помощью dynamic_cast
, и не обязательно иметь дело с более соответствующий дизайн.