3

У меня есть ситуация, как показано ниже:Требуется ли множественное наследование?

class A { 
    virtual void f() { /*some default impl here*/} 
    virtual void g() { /*some default impl here*/} 
}; 

class B : public A { 
    virtual void f() { /* do some specific stuff and call parent's f()*/} 
    virtual void g() { /* do some specific stuff and call parent's g()*/} 
}; 

class C : public A { 
    virtual void f() { /* do some specific stuff and call parent's f()*/} 
    virtual void g() { /* do some specific stuff and call parent's g()*/} 
}; 

class mixed /* I don't know how to derive*/ { 
    virtual void f() { /* call B::f()*/} 
    virtual void g() { /* call C::g()*/} 
}; 

Я думаю о множественном наследовании здесь. I.e., составляют mixed, полученные от B и C. Но есть некоторые известные проблемы (например, Diamond problem).

Другим решением может быть композиция.

Но что является правильным решением, пожалуйста, сообщите :)

Спасибо заранее!

+1

Если 'A' - это просто интерфейс (т. Е. Он не имеет значимых элементов данных), вы можете просто сделать' mixed' inerherit от 'A', а затем иметь два элемента данных:' B b; 'и' C c; '. Затем в своем 'f()' вы вызываете 'b.f()' и в 'g()' вы вызываете 'c.g()'. Или, возможно, вы можете разделить 'A' на два разных класса: один с' f() 'и один с' g() ', а затем два разных' B '' и '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' ' , из которого «смешанный» мог наследовать. – Cornstalks

ответ

3

Тот факт, что каждый метод должен «делать материал» перед вызовом родителя, вызывает проблему.

Одним из решений было бы иметь A и B в качестве членов класса mixed. После этого вы можете контролировать то, что вам нужно сделать с ними в mixed::f() и mixed::g()

Если вам нужно, вы можете создать базовый класс base с чисто виртуальных функцийf() и g(). mixed может наследовать от этого, и так может A, B и C. Вы ссылаетесь на эту возможность, когда вы обсуждаете состав.

3

Я думаю, вы могли бы найти что-то вроде этого. (К сожалению, это большая куча коды, но это действительно просто.)

#include <iostream> 

struct A 
{ 
    virtual void 
    f() 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    } 

    virtual void 
    g() 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    } 

    // Don't forget the virtual destructor. 
    virtual ~A() noexcept = default; 
}; 

struct B : virtual A 
{ 
    virtual void 
    f() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    A::f(); 
    } 

    virtual void 
    g() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    A::g(); 
    } 
}; 

struct C : virtual A 
{ 
    virtual void 
    f() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    A::f(); 
    } 

    virtual void 
    g() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    A::g(); 
    } 
}; 

struct D : virtual B, virtual C 
{ 
    virtual void 
    f() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    B::f(); 
    } 

    virtual void 
    g() override 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    C::g(); 
    } 
}; 

int 
main() 
{ 
    D d {}; 
    d.f(); 
    std::cout << '\n'; 
    d.g(); 
} 

override особенность C++ 11, чтобы компилятор проверить, вы на самом деле переопределение. Использование его - хорошая практика, но не требуется, если ваш компилятор его не поддерживает. __PRETTY_FUNCTION__ является расширением GCC, чтобы получить строковый литерал, который называет подпись текущей функции. Стандарт C++ имеет __func__, но здесь он менее полезен. Вы можете набирать строки самостоятельно, если у вашего компилятора нет функции, сравнимой с __PRETTY_FUNCTION__.

Выход:

virtual void D::f() 
virtual void B::f() 
virtual void A::f() 

virtual void D::g() 
virtual void C::g() 
virtual void A::g() 

Это работает, но я не считаю это довольно код. Композиция, вероятно, будет лучшим решением здесь.

1

Вот является альтернативой виртуального наследования: с помощью CRTP смешать в функциональности B и C в M, разделяя общий A, без накладных расходов виртуальных таблиц.

#include <iostream> 

struct A 
{ 
    int a; 
}; 

template <typename T> 
struct B 
{ 
    A *get_base_a() {return static_cast<T*>(this);} 
    void print_a() {std::cout << get_base_a()->a << '\n';} 
}; 

template <typename T> 
struct C 
{ 
    A *get_base_a() {return static_cast<T*>(this);} 
    void print_a() {std::cout << get_base_a()->a << '\n';} 
}; 

struct M : A, B<M>, C<M> 
{ 
}; 

int main() 
{ 
    M m; 
    m.a = 1; 
    m.B::print_a(); 
    m.C::print_a(); 
    return 0; 
} 

Однако обратите внимание, что вы не будете в состоянии передать M* в функцию, которая ожидает B* или C*.

3

некоторые известные проблемы (например, проблема с алмазами).

Во-первых: Нет рисунка с бриллиантом, если вы явно не создали его.

class mixed: public B, public C 

Это приведет к смешанному наследованию от B и C. Каждый из них имеет свой собственный явный A (без алмаза).

Поскольку оба B и C имеют виртуальные элементы, полученные из A, он становится неоднозначным, который следует вызывать, но вы решили, что с явным определением всех виртуальных объектов в mixed (так проблема решена).

void mixed::f() { B::f();} // works fine. 

Теперь, даже если вы явно создаете алмаз.

Примечание: Диаграмма алмаза не отображается нормально. Алмазный шаблон - это дизайнерское решение, которое должно быть явно сделать, и вы используете его для решения определенных типов проблем (с помощью виртуального наследования).

class B: public virtual A ... 
class C: public virtual A ... 
class mixed: public B, public C ... 

У вас по-прежнему нет проблем. Потому что mixed::f() использует только ветвь B (а затем A). В то время как mixed::g() использует только ветвь C (а затем A).

Даже если A имеет свое собственное состояние (хотя это, вероятно, плохая идея, как правило, лучше всего использовать интерфейсы в качестве виртуального базового класса), то мы не имеем проблему, потому что mixed::f() и mixed::g() только вызвать функцию у одного ребенка (проблемы начинают возникать, если они вызывают обе стороны и состояние A становится мутирован обоих вызовов.

Другим решением может быть композиция.

Это также будет работать.

class mixed: public A 
{ 
    B b; 
    C c; 
    public: 
     virtual void f() override {b.f();} 
     virtual void g() override {c.g();} 
    .... 
}; 

Но что такое правильное решение

Там нет правильного решения. Это будет зависеть от деталей, которые вы не упомянули (например, какие детали A).

BUT Генеральный совет состоит в том, чтобы отдать предпочтение композиции над наследованием, но это только общие рекомендации всегда будут сводиться к решению реальной проблемы.