Множественное наследование от одного и того же прародителя - объединить реализации?

для определенного проекта я объявил интерфейс (класс только с чисто виртуальными функциями) и хочу предложить пользователям некоторые реализации этого интерфейса.

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

Однако я также хочу представить пользователям полностью пригодную для использования реализацию интерфейса. Итак, мой первый подход состоял в том, чтобы просто получить класс из обеих частичных реализаций. Это не сработало и завершилось с ошибкой, что некоторые функции все еще являются чисто виртуальными в производном классе.

Итак, мой вопрос заключается в том, есть ли способ просто объединить две частичные реализации одного и того же интерфейса. Я нашел обходной путь, явно указав, какую функцию я хочу вызывать для каждого метода, но я считаю это довольно уродливым и был бы признателен за механизм, который позаботится об этом за меня.

#include <iostream>

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

class B: public A{
    public:
        void foo(){ std::cout << "Foo from B" << std::endl; }
};

class C: public A{
    public:
        void bar(){ std::cout << "Bar from C" << std::endl; }
};

// Does not work
class D: public B, public C {};

// Does work, but is ugly
class D: public B, public C {
    public:
        void foo(){ B::foo(); }
        void bar(){ C::bar(); }
};

int main(int argc, char** argv){
    D d;
    d.foo();
    d.bar();
}

С уважением, Александр


Настоящая проблема заключается в управлении несколькими посетителями дерева, позволяя каждому из них пройти по дереву, принять решение для каждого из узлов, а затем агрегировать решение каждого посетителя и собрать его в определенное решение.

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


person aweinert    schedule 18.04.2011    source источник
comment
Я думаю, что ключевое слово using будет немного менее уродливым, чем то, что у вас есть, но оно не отвечает на ваш вопрос.   -  person Andrew Rasmussen    schedule 18.04.2011
comment
Итак, как именно я должен использовать ключевое слово using? Это избавило бы меня от B:: и C::, но мне все равно пришлось бы написать что-то вроде строк void foo(){foo();} и void bar(){bar();}   -  person aweinert    schedule 19.04.2011


Ответы (5)


Рассматривали ли вы возможность полного отказа от алмазного наследования, предоставляя несколько абстрактных классов, каждый с дополнительными реализациями, позволяя пользователю смешивать и сопоставлять реализацию и интерфейс по умолчанию по мере необходимости?

В вашем случае происходит следующее: когда вы наследуете D, B::bar не реализовано, а C::foo не реализовано. Промежуточные классы B и C не могут видеть реализации друг друга.

Если вам нужен полный интерфейс в прародителе, рассматривали ли вы возможность предоставления реализации другим способом, возможно, в виде политики с шаблонами и классами по умолчанию, которые будут отправлены для обеспечения поведения по умолчанию?

person Mark B    schedule 18.04.2011
comment
Спасибо за ваши идеи, но, как я (сейчас) указал выше, разделение интерфейса, к сожалению, невозможно без огромных накладных расходов. Не могли бы вы предоставить дополнительную информацию или ссылку о том, как обеспечить реализацию с использованием шаблонов? - person aweinert; 19.04.2011

Если ваш интерфейс верхнего уровня имеет логическое разделение по функциональности, вам следует разделить его на два отдельных интерфейса. Например, если у вас есть функции сериализации и рисования в интерфейсе A, вы должны разделить их на два интерфейса: ISerialization и IDrawing.

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

person Nathanael    schedule 18.04.2011
comment
Спасибо, но в этой конкретной проблеме разделение невозможно. Я добавил дополнительную информацию о реальной проблеме выше. - person aweinert; 19.04.2011

Существует также вероятность того, что вы могли бы использовать «фабричный» класс для основного типа интерфейса. Другими словами, основной класс интерфейса также содержит некоторый тип статической функции, которая генерирует соответствующий дочерний класс по запросу пользователя. Например:

#include <cstdio>

class A 
{
    public:
       enum class_t { CLASS_B, CLASS_C };

       static A* make_a_class(class_t type);

       virtual void foo() = 0;
       virtual void bar() = 0;
};

class B: public A
{
    private:
        virtual void foo() { /* does nothing */ }

    public:
        virtual void bar() { printf("Called B::bar()\n"); }
};

class C: public A
{
    private:
        virtual void bar() { /* does nothing */ }

    public:
        virtual void foo() { printf("Called C::foo()\n"); }
};

A* A::make_a_class(class_t type)
{
   switch(type)
   {
       case CLASS_B: return new B();
       case CLASS_C: return new C();
       default: return NULL;
    }
}

int main()
{
    B* Class_B_Obj = static_cast<B*>(A::make_a_class(A::CLASS_B));
    C* Class_C_Obj = static_cast<C*>(A::make_a_class(A::CLASS_C));

    //Class_B_Obj->foo(); //can't access since it's private
    Class_B_Obj->bar();

    Class_C_Obj->foo();
    //Class_C_Obj->bar(); //can't access since it's private

    return 0;
}

Если class A по какой-то причине требуется доступ к некоторым закрытым членам class B или class C, просто сделайте class A другом дочерних классов (например, вы можете сделать конструкторы class B и class C частными конструкторами, чтобы только статическая функция в class A могла генерировать их, и пользователь не может создать их самостоятельно, не вызывая статическую фабричную функцию в class A).

Надеюсь это поможет,

Джейсон

person Jason    schedule 18.04.2011
comment
Поправьте меня, если я ошибаюсь, но на самом деле это не решит задачу слияния реализаций в B и C, не так ли? Таким образом, я смогу создать только частичные реализации. Это было бы именно то, чего я пытался избежать. - person aweinert; 19.04.2011
comment
Что ж, это возможно, вам нужно будет создать class D со связанным перечислением CLASS_D, которое будет представлять ваш составной класс. Этот составной класс, вероятно, должен будет использовать ромбовидную структуру наследования, которую вы показали в своем первом посте, но, по крайней мере, вы предоставите конечному пользователю унифицированный интерфейс для создания объектов. Кроме того, в зависимости от реализации, им не придется беспокоиться о деталях того, какой тип класса они получают, а скорее они получат класс с согласованным интерфейсом для всех возвращаемых типов. от единственной заводской функции. - person Jason; 19.04.2011
comment
Быстрый вопрос: вам нужно только вызывать функции из объединенного интерфейса, или есть также общие элементы данных, для которых требуется метод множественного наследования, который вы описали выше? - person Jason; 20.04.2011
comment
Нет, мне нужны только функции. Каждая частичная специализация использует некоторые данные участников, которые не должны быть доступны для D. - person aweinert; 20.04.2011
comment
Хорошо, я собираюсь добавить еще один ответ ниже, который может решить вашу проблему с помощью шаблонов. - person Jason; 20.04.2011

Поскольку вы упомянули, что вам в основном нужен доступ к функциям, а не к данным-членам, вот еще один метод, который вы могли бы использовать вместо множественного наследования с использованием шаблонов и частичной специализации шаблона:

#include <iostream>

using namespace std;

enum class_t { CLASS_A, CLASS_B, CLASS_C };

template<class_t class_type>
class base_type
{
    public:
            static void foo() {}
            static void bar() {}
};

template<>
void base_type<CLASS_A>::foo() { cout << "Calling CLASS_A type foo()" << endl; }

template<>
void base_type<CLASS_B>::bar() { cout << "Calling CLASS_B type bar()" << endl; }

template<>
void base_type<CLASS_C>::foo() { base_type<CLASS_A>::foo(); }

template<>
void base_type<CLASS_C>::bar() { base_type<CLASS_B>::bar(); }

int main()
{
    base_type<CLASS_A> Class_A;
    Class_A.foo();

    base_type<CLASS_B> Class_B;
    Class_B.bar();

    base_type<CLASS_C> Class_C;
    Class_C.foo();
    Class_C.bar();

    return 0;
}

Теперь, если вам нужны нестатические функции, которые имеют доступ к закрытым членам данных, это может стать немного сложнее, но все же выполнимо. Хотя, скорее всего, потребуется отдельный класс признаков, который вы можете использовать для доступа к нужным типам, не сталкиваясь с ошибками компилятора «неполные типы».

Спасибо,

Джейсон

person Jason    schedule 20.04.2011

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

Использование виртуального наследования решает проблему, поскольку гарантирует наличие только одной копии A в D.

person Clément    schedule 05.03.2015