Множественное наследование и чистые виртуальные функции - программирование
Подтвердить что ты не робот

Множественное наследование и чистые виртуальные функции

Следующий код:

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. Я пропустил виртуальные деструкторы с целью сделать код короче. Пожалуйста, не говорите мне, чтобы я их ввел, я уже знаю:)

4b9b3361

Ответ 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, и не обязательно иметь дело с более соответствующий дизайн.