Множественное наследование C++: реализация интерфейсов с перекрывающимися виртуальными функциями

Я пытаюсь разработать иерархию классов на C++, где у меня есть базовый интерфейс FooInterface с чистой виртуальной функцией foo() и другой интерфейс FooBarInterface, который должен расширяться FooInterface и добавлять дополнительную функцию bar(). Затем у меня есть класс Foo, который реализует foo() из FooInterface, и класс FooBar, который наследуется от Foo и FooBarInterface и реализует bar(). Моя цель — сделать так, чтобы функция func принимала std::shared_ptr к любому объекту, который реализует как foo(), так и bar().

#include <memory>

class FooInterface {
public:
    virtual void foo() = 0;
};

class FooBarInterface : public FooInterface {
public:
    virtual void bar() = 0;
};

class Foo : public FooInterface {
public:
    void foo() override {};
};

class FooBar : public Foo, public FooBarInterface {
public:
    void bar() override {};
};

void func(std::shared_ptr<FooBarInterface> fb) {}

int main() {
    func(std::make_shared<FooBar>());
}

Примечание: виртуальный деструктор опущен.

Однако этот код не компилируется, вероятно, потому, что foo() в FooBarInterface не переопределяется в FooBar, хотя FooBar наследуется от Foo, который реализует foo().

Как мне изменить свой код? Или у вас есть какой-то шаблон проектирования, на который мне следует сослаться?

@user12002570 user12002570, но почему в этом случае FooBar не получает реализацию foo от Foo? (все три основных компилятора жалуются, что FooBar является абстрактным - см. демо). Я не увидел объяснений в дураке.

wohlstad 11.08.2024 16:25

Открыт вновь. Да, буквальный ответ содержится в вопросе, указанном пользователем @user12002570. Но есть более тонкая проблема, которую следует решить; если бы FooInterface был виртуальной базой как Foo, так и FooBarInterface, код был бы действителен благодаря доминированию.

Pete Becker 11.08.2024 16:59

@PeteBecker Я действительно заметил это, пока «играл» с примером. Но почему FooBar абстрактно как есть (без виртуального наследования)? Почему он не наследует foo от Foo?

wohlstad 11.08.2024 17:03

@wohlstad - в коде в вопросе есть две разные FooInterface::foo функции: одна проходит Foo и одна проходит FooBarInterface. Второе не переопределяется, поэтому FooBar по-прежнему абстрактно. Когда база FooInterface виртуальная, FooInterface::foo переопределяет foo в обоих промежуточных классах (именно это и означает «доминирование»).

Pete Becker 11.08.2024 17:08

@PeteBecker Теперь я понял. Я пропустил существование 2 foos. Возможно, вы захотите опубликовать ответ, потому что, насколько я понимаю, предложенный обман не решает эту проблему.

wohlstad 11.08.2024 17:10

Вздох. Пожалуйста, игнорируйте ссылку «доминирование» в моем комментарии «Возобновлено». Страница, на которую он указывает, — ерунда.

Pete Becker 11.08.2024 17:32
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
61
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Если вы хотите, чтобы FooBar::foo использовал реализацию Foo, вы вызываете его.

class FooBar : public Foo, public FooBarInterface {
public:
    void foo() override { Foo::foo(); } 
    void bar() override {};
};

Ответ на самом деле не объясняет, почему FooBar является абстрактным как есть (без добавления foo). См. ответ ПитеБекера для объяснения (оно также отсутствовало в моем ответе, поэтому я удалил его).

wohlstad 11.08.2024 17:47
Ответ принят как подходящий

Я запутался в Foo, Bar и Interfaces, поэтому собираюсь переписать иерархию классов, сохранив ту же структуру:

struct Base {
    virtual void foo() = 0;
}

struct Intermediate1 : Base {
};

struct Intermediate2 : Base {
    void foo();
}

struct Derived : Intermediate1, Intermediate2 {
};

Derived d; // error: can't create object of an abstract type

Объект Derived имеет два подобъекта типа Base, один проходит через Intermediate1, а другой проходит через Intermediate2. Таким образом, у него есть две разные функции foo: одна проходная Intermediate1 и одна проходная Intermediate2. Поскольку Intermediate1 не переопределяет Base::fooDerived тоже не переопределяет ее), это по-прежнему чисто виртуальная функция, поэтому Derived является абстрактным классом и не может быть создан экземпляр.

С другой стороны (и сколько людей представляют себе наследование «интерфейсов» (цитируется потому, что в C++ нет формального понятия «интерфейс»)) подобная иерархия может выглядеть так:

struct Base {
    virtual void foo() = 0;
}

struct Intermediate1 : virtual Base {
};

struct Intermediate2 : virtual Base {
    void foo();
}

struct Derived : Intermediate1, Intermediate2 {
};

Derived d; // okay, via "dominance"

Обратите внимание на добавленный virtual для классов, производных непосредственно от Base. Теперь объект типа Derived имеет только один подобъект типа Base, а правило доминирования гласит, что в DerivedIntermediate2::foo переопределяет Base::foo, хотя в Intermediate1 переопределения нет. Итак, Intermediate1 сам по себе является абстрактным классом, а Derived — нет, хотя он и не переопределяет Base::foo; переопределения через Intermediate2 достаточно.

Хорошее объяснение. +1. Я бы просто добавил примечание, что использование shared_ptr здесь не имеет особого значения, а только тот факт, что FooBar является абстрактным (поскольку проблема использования ptr была явно упомянута в вопросе).

wohlstad 11.08.2024 17:31

Также обратите внимание, что виртуальное наследование — это проектное решение, а не решение по кодированию. Не делайте базы виртуальными только потому, что это «решает» насущную проблему; сделайте базу виртуальной, потому что это подходит для проектируемой иерархии. Пример Бьерна: если у вас есть класс списка Node в качестве основы (интрузивный список), вы должны написать struct X : Node ... и struct Y : Node, когда хотите создать список X и список Y. Если у вас есть производный класс, который одновременно является X и Y, вам нужны два Node класса, а не один, чтобы вы могли использовать свой список X и список Y.

Pete Becker 11.08.2024 19:16

Другие вопросы по теме