Множественное наследование + беспорядок виртуальных функций - PullRequest
16 голосов
/ 05 марта 2009

У меня есть сценарий множественного наследования с бриллиантами, подобный этому:

    A
  /   \
 B     C
  \   /
    D

Общий родитель A определяет виртуальную функцию fn ().
Возможно ли для B и C определить fn()?
Если это так, то следующий вопрос - может ли D обращаться к fn () как B, так и C без неоднозначности? Я предполагаю, что есть некоторый синтаксис для этого ..
И возможно ли для D сделать это, не зная конкретно, кто такие B и C? B и C можно заменить некоторыми другими классами, и я хочу, чтобы код в D был универсальным.

Я пытаюсь сделать так, чтобы D каким-то образом перечислял все экземпляры функции fn (), которые он имеет в своем происхождении. Возможно ли это в каких-то других средствах виртуальных функций?

Ответы [ 6 ]

18 голосов
/ 05 марта 2009

Если вы не перезапишите fn снова в D, нет, это невозможно. Потому что в объекте D нет окончательного переопределения: и C, и B переопределяют A::fn. У вас есть несколько вариантов:

  • Падение либо C::fn, либо B::fn. Затем тот, который все еще переопределяет A::fn, имеет последнее переопределение.
  • Поместите последний переопределитель в D. Затем этот переопределяет A::fn, а также fn в C и B.

Например, следующие ошибки приводят к ошибке времени компиляции:

#include <iostream>

class A {
public:
    virtual void fn() { }
};

class B : public virtual A {
public:
    virtual void fn() { }
};

class C : public virtual A {
public:
    virtual void fn() { }
};

// does not override fn!!
class D : public B, public C {
public:
    virtual void doit() {
        B::fn();
        C::fn();
    }
};

int main(int argc, char **argv) {
  D d;
  d.doit();
  return 0;
}

Тем не менее, вы можете получить не-виртуальный из A в C и B, но тогда у вас больше нет наследования алмазов. То есть каждый элемент данных в A появляется дважды в B и C, потому что у вас есть два подобъекта базового класса A в объекте D. Я бы порекомендовал вам переосмыслить этот дизайн. Попробуйте исключить двойные объекты, подобные тем, которые требуют виртуального наследования. Это часто вызывает такие конфликтные ситуации.

Случай, очень похожий на это, когда вы хотите переопределить определенную функцию. Представьте, что у вас есть виртуальная функция с тем же именем в B и C (теперь без общей базы A). И в D вы хотите переопределить каждую функцию, но дать разное поведение каждой. В зависимости от того, вызываете ли вы функцию с указателем B или C, у вас может быть другое поведение. Множественное наследование, часть III Херба Саттера описывает хороший способ сделать это. Это может помочь вам определиться с дизайном.

4 голосов
/ 05 марта 2009

Первый вопрос, да, B и C могут определить fn() как виртуальную функцию. Во-вторых, D может, конечно, получить доступ к B::fn() и C::fn() с помощью оператора видимости :: Третий вопрос: D, по крайней мере, должен знать B и C, так как вы должны определить их в списке наследования. Вы можете использовать шаблоны, чтобы открыть типы B и C:

class A
{
public:
   virtual ~A() {}
   virtual void fn() = 0;
};

class B: public A
{
public:
   virtual ~B() {}
   virtual void fn(){ std::cout << "B::fn()" << std::endl; }
};

class C: public A
{
public:
   virtual ~C() {}
   virtual void fn(){ std::cout << "C::fn()" << std::endl; }
};

template <typename TypeB, typename TypeC>
class D: public TypeB, public TypeC
{
public:
   void Do()
   {
      static_cast<TypeB*>(this)->fn();
      static_cast<TypeC*>(this)->fn();
   }
};

typedef D<B, C> DInst;

DInst d;
d.Do();

О желании автоматически перечислить все функции fn () всех классов, от которых наследуется D: я не уверен, возможно ли это, не прибегая к MPL. По крайней мере, вы можете расширить мой пример выше версиями, которые работают с 3 и более параметрами шаблона, но я предполагаю, что существует верхний (внутренний компилятор) предел количества параметров шаблона класса.

2 голосов
/ 06 марта 2009

Возможно, вы захотите взглянуть на Loki TypeLists, если вам действительно необходимо отслеживать происхождение и перечислять типы. Я не уверен, что то, о чем ты просишь, действительно возможно без кучки работы. Удостоверьтесь, что вы не перегружены здесь.

На немного другой ноте, если вы собираетесь использовать MI таким образом (т. Е. страшный бриллиант ), тогда вы должны четко указать, какой виртуальный член вам нужен. Я не могу вспомнить хорошего случая, когда вы хотите выбрать семантику B::fn() вместо C::fn() без явного принятия решения при написании D. Вы, вероятно, выберете одно над другим (или даже оба) в зависимости от того, что делает отдельный метод. После того как вы приняли решение, требуется, чтобы унаследованные изменения не изменили ожидания или семантический интерфейс.

Если вы действительно беспокоитесь о замене в новом классе, скажем E вместо B, где E не происходит от B, но предлагает тот же интерфейс, тогда вам действительно следует использовать шаблон подход, хотя я не уверен, почему там есть static_cast<> ...

struct A {
    virtual ~A() {}
    virtual void f() = 0;
};
struct B: A {
    virtual void f() { std::cout << "B::f()" << std::endl; }
};
struct C: A {
    virtual void f() { std::cout << "C::f()" << std::endl; }
};

template <typename Base1, typename Base2>
struct D: Base1, Base2 {
    void g() { Base1::f(); Base2::f(); }
};

int main() {
    D<B,C> d1;
    D<C,B> d2;
    d1.g();
    d2.g();
    return 0;
}

// Outputs:
//   B::f()
//   C::f()
//   C::f()
//   B::f()

отлично работает и выглядит немного проще.

1 голос
/ 05 марта 2009

Вы не можете перечислить определения fn () в происхождении. С ++ не хватает отражения. Единственный способ, который я могу себе представить, - это гигантская петля, проверяющая типид всех возможных предков И больно это себе представлять.

1 голос
/ 05 марта 2009

Уже есть несколько вопросов, которые касаются этого. Похоже, у нас заканчиваются вопросы. Возможно, поле поиска должно быть больше, чем кнопка Задать вопрос.

См

0 голосов
/ 05 марта 2009

Vividos уже ответил на основную часть поста. Даже если бы я использовал оператор области вместо более громоздкого оператора разыменования static_cast <> +.

В зависимости от поставленной задачи, возможно, вы можете изменить отношение наследования с D на B и C для меньшего состава связывания (плюс, возможно, наследование от A). Это предполагает, что вам не нужно, чтобы D использовался полиморфно как B или C, и что вам не нужно, чтобы B и C совместно использовали один и тот же базовый экземпляр.

Если вы выберете композицию, вы можете получить B и C в качестве аргументов вашего конструктора в качестве ссылок / указателей типа A, в результате чего D совершенно не будет знать о типах B и C. В этот момент вы можете использовать контейнер удерживайте как можно больше производных объектов. Ваша собственная реализация fn () (если вы так решите) или любого другого метода.

...