C ++ множественное наследование + виртуальные функции (- неоднозначность) = странное поведение (также указатели функций) - PullRequest
2 голосов
/ 02 февраля 2012

Я создаю пару интерфейсов, предназначенных для обеспечения доступа к функциям обратного вызова. То есть наследование от интерфейса A позволяет классу использовать обратные вызовы типа один; интерфейс B допускает второй тип. Наследование от A и B позволяет выполнять обратные вызовы обоих типов. Конечная цель состоит в том, чтобы классы A и B позаботились обо всей грязной работе, просто унаследовав от них.

Первая проблема

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

class A
{
public:
    static void AFoo( void* inst )
    {
        ((A*)inst)->ABar( );
    }
    virtual void ABar( void ) = 0;
};

class B
{
public:
    static void BFoo( void* inst )
    {
        ((B*)inst)->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A, public B
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};

И совершая звонки

C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo( (void*)c_inst );
BFoo( (void*)c_inst );

Я ожидаю, что я получу "AB" в качестве вывода. Вместо этого я получаю «АА». Меняя порядок производных классов (B перед A), выдает «BB». Почему это?

Вторая проблема

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

template <class T> class A
{
public:
    static void AFoo( void* inst )
    {
        ((T*)inst)->ABar( );
    }
    virtual void ABar( void ) = 0;
};

template <class T> class B
{
public:
    static void BFoo( void* inst )
    {
        ((T*)inst)->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A<C>, public B<C>
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};

Причина этого в том, что A и B могут выполнять всю работу, но для их реализации не требуется знание C.

Теперь звоните с

C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo( (void*)c_inst );
BFoo( (void*)c_inst );

выдает правильный вывод: "AB".

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

Например, я могу успешно вызвать C :: AFoo (), но не всегда C :: BFoo (). И иногда это зависит от порядка, в котором я наследую A и B: class C: public A<C>, public B<C> может генерировать код, в котором не работают ни AFoo, ни BFoo, тогда как class C: public B<C>, public A<C> может генерировать код, в котором работает один из них, или, возможно, оба.

Поскольку классы являются шаблонными, я могу удалить виртуальные функции в A и B. В результате получается рабочий код, если, конечно, в C существуют ABar и BBar. Это приемлемо, но не желательно; Я бы лучше знал, в чем проблема.

Каковы возможные причины того, что приведенный выше код может вызвать странные проблемы?

Почему второй пример выдает правильный вывод, а первый - нет?

Ответы [ 2 ]

5 голосов
/ 02 февраля 2012

Вы вызываете неопределенное поведение. Вы можете разыграть X* до void*, но как только вы это сделаете, единственное, что можно безопасно разыграть, void* - это X* (это не совсем верно, я упрощаю но ради аргумента притворимся, что это так).

Теперь, почему код ведет себя так, как он есть? Один из способов реализации MI - это что-то вроде:

 struct A
 {
    A_vtable* vtbl;
 };

 struct B
 {
    B_vtable* vtbl;
 };

 struct C
 {
    struct A;
    struct B;
 };

В этом примере A является первым, но порядок будет определяться компилятором. Когда вы разыгрываете void, вы получаете указатель на начало C. Когда вы разыгрываете void * обратно, вы теряете информацию, необходимую для правильной настройки указателя, если это необходимо. Поскольку и A, и B имеют одну виртуальную функцию с одной и той же сигнатурой, в итоге вы вызываете impl. того класса, который окажется первым в макете объекта.

1 голос
/ 02 февраля 2012

Как сказал Логан Капальдо, у этого подхода реализации обратных вызовов есть проблемы.Преобразование void * в XXX * небезопасно, поскольку мы не могли гарантировать, что приведенный указатель действительно указывает на XXX (он может указывать на другой тип или даже недопустимый адрес, что приведет к непредсказуемым проблемам).Мое предложение заключается в изменении типа аргумента вашей статической функции на тип интерфейса, а именно:

class A
{
public:
    static void AFoo( A* inst )
    {
        inst->ABar( );
    }
    virtual void ABar( void ) = 0;
};

class B
{
public:
    static void BFoo( B* inst )
    {
         inst->BBar( );
    }
    virtual void BBar( void ) = 0;
};

class C : public A, public B
{
public:
    void ABar( void ){ cout << "A"; };
    void BBar( void ){ cout << "B"; };
};


C* c_inst = new C( );
void (*AFoo) (void*) = C::AFoo;
void (*BFoo) (void*) = C::BFoo;
AFoo(c_inst );
BFoo(c_inst );

Это преимущества: во-первых, не требуется void * приведение.Во-вторых, это заставляет пользователей передавать правильный тип указателя.И пользователи знают точный тип аргумента этих статических функций без какой-либо документации.

...