C++ множественное наследование и чистые функции

Меня немного смущает "проблема множественного наследования". Рассмотрим следующий кусок кода:

#include <iostream>

struct iface
{
    virtual void foo () = 0;
    virtual ~iface () {}
};

struct wrapped
{
    virtual void foo ()
    {
        std::cerr << "wrapped\n";
    }
};

struct impl : public wrapped, public iface
{
};

iface* factory ()
{
    return new impl;
}

int main ()
{
    iface *object = factory ();

    object->foo();

    delete object;

    return 0;
}

Это абстрагируется от некоторого кода, использующего идиому pimpl. Идея в том, что wrapped — это какой-то сложный класс со всякими другими наворотами. iface просто делает определенный аспект wrapped видимым, а функция factory() создает класс impl, который реализует iface, используя wrapped. В моем реальном коде класс wrapped происходит из библиотеки с огромным деревом заголовков, поэтому я определяю impl в отдельной единице компиляции, чтобы остальная часть моего приложения не использовала их.

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

$ g++ -o test test.cc
test.cc: In function ‘iface* factory()’:
test.cc:23:16: error: cannot allocate an object of abstract type ‘impl’
     return new impl;
                ^
test.cc:17:8: note:   because the following virtual functions are pure within ‘impl’:
 struct impl : public wrapped, public iface
        ^
test.cc:5:18: note:     virtual void iface::foo()
     virtual void foo () = 0;
                  ^

Очевидно, что я могу избежать ошибки, написав конкретную реализацию foo в impl, которая пересылает в wrapped::foo(), но я не понимаю, почему компилятор не подхватывает реализацию из класса wrapped.

Я предполагаю, что каким-то образом "foo" из iface и "foo" из wrapped оказываются разными при разрешении имен (это правильное слово) для impl, но я действительно не понимаю, почему.

Может кто-нибудь объяснить, что происходит?


person Rupert Swarbrick    schedule 18.04.2014    source источник
comment
Происходит именно это: wrapper::foo() волшебным образом не сопоставляется с iface::foo(). Это два разных типа. При использовании в качестве iface компилятор должен иметь возможность разрешать iface::foo(), он не может просто догадаться, что вызывается унаследованная wrap::foo(), вам придется предоставить реализацию.   -  person Willem van Rumpt    schedule 18.04.2014
comment
Да, это имеет смысл. Я предполагаю, что проблема в том, что impl ниже по течению как от iface, так и от wrapped, поэтому нет способа убедить их двоих в том, что их foo функции должны иметь одно и то же разрешенное имя (если это правильная терминология). Спасибо за помощь.   -  person Rupert Swarbrick    schedule 18.04.2014


Ответы (2)


Если какая-либо виртуальная функция класса является чистой виртуальной, то класс является абстрактным. Класс impl имеет две функции-члена с одинаковыми именами.

iface::foo

а также

wrapped::foo

Первая функция-член класса impl является чисто виртуальной, поэтому класс является абстрактным. Компилятор не может создавать объект абстрактного класса.

person Vlad from Moscow    schedule 18.04.2014
comment
Верно. Извините, наверное, я недостаточно ясно выразился в своем вопросе. Я пытался понять, возможно ли объявить мой класс impl так, чтобы две функции получили одно и то же имя. Очевидно нет. - person Rupert Swarbrick; 18.04.2014
comment
В любом случае, спасибо. Это правильно, и (с комментарием Виллема ван Румпта) теперь я понимаю, что происходит. Спасибо вам обоим за помощь. - person Rupert Swarbrick; 18.04.2014

impl имеет два совершенно отдельных метода: iface::foo, который является чистым, и wrapped::foo. Разрешения имен нет — вызовы виртуальных функций отправляются структурой vtable, которая содержит указатели на реализации. У класса есть отдельная виртуальная таблица для каждого из его прямых предков, по крайней мере, в случаях без виртуального наследования. Синонимичные виртуальные функции от разных предков попадают в разные виртуальные таблицы; компилятор не может их "объединить". C++ не имеет миксинов.

Очевидно, что я могу избежать ошибки, написав конкретную реализацию foo в impl, которая переадресовывает в wrapd::foo().

А еще лучше не используйте двойное наследование — сделайте что-нибудь вроде

struct impl : public iface
{
    virtual void foo ()
    {
        wr.foo ();
    }
private:
    wrapped wr;
};
person Michael Ivko    schedule 18.04.2014
comment
Совершенно серьезно: почему вы считаете, что хранение объекта — лучшее решение? Я понимаю некоторые проблемы с множественным наследованием (ромбовидные диаграммы и т. д.), но я предположил, что приведенный выше пример был именно той проблемой, для решения которой было разработано множественное наследование. Должен сказать, что я родом из lisp, где система объектов значительно более гибкая (и, соответственно, медленнее), поэтому некоторые из них этого не делают! советы в кругах С++ меня довольно сбивают с толку. - person Rupert Swarbrick; 18.04.2014
comment
Вы не должны использовать множественное наследование, потому что это сбивает с толку и не дает ничего, чего нельзя было бы сделать с помощью простой агрегации. Некоторые функции C++ плохо разработаны. Вы можете использовать указатель, если wrapper тяжелый. - person Michael Ivko; 18.04.2014
comment
ХОРОШО. Мы собираемся принципиально не согласиться с этим выбором дизайна, поэтому я оставлю разговор на этом. Спасибо за уточнение. - person Rupert Swarbrick; 18.04.2014