Ниже приведен код. Я не понимаю, почему он ведет себя так:

#include <iostream>
using namespace std;

class FooInterface {
public:
    virtual ~FooInterface() = default;
    virtual void Foo() = 0;
};

class BarInterface {
public:
    virtual ~BarInterface() = default;

    virtual void Bar() = 0;
};

class Concrete : public FooInterface, public BarInterface {
public:
    void Foo() override { cout << "Foo()" << endl; }
    void Bar() override { cout << "Bar()" << endl; }
};

int main() {
    Concrete c;
    c.Foo();
    c.Bar();

    FooInterface* foo = &c;
    foo->Foo();

    BarInterface* bar = (BarInterface*)(foo);
    bar->Bar(); // Prints "Foo()" - WTF?
}

Последний оператор bar->Bar() печатает «Foo()», что меня смутило. Это взято из следующего блога: https://shaharmike.com/cpp/vtable-part4/. В основном это относится к структуре vtable класса и к тому, как компилятор обрабатывает приведение между двумя родительскими классами мультинаследуемого класса с виртуальными функциями. Может ли кто-нибудь помочь мне понять это?

0
Optimus Prime 23 Мар 2020 в 01:18
BarInterface* bar = dynamic_cast(foo);
 – 
Minor Threat
23 Мар 2020 в 01:21
BarInterface* bar = dynamic_cast(foo); печатает «Bar()», но мой код печатает «Foo()». Таким образом, приведение типа C не выполняет динамическое приведение.
 – 
Optimus Prime
23 Мар 2020 в 01:24
1
Ваша проблема требует динамического анализа динамического типа объекта. Приведение в стиле C здесь не работает, так как оно просто отключает проверку типов и возвращает тот же указатель, который указывает на подобъект FooInterface.
 – 
Minor Threat
23 Мар 2020 в 01:31

1 ответ

Когда вы пишете (BarInterface*)(foo);, вы лжете компилятору. Вы говорите ему, что foo на самом деле является указателем на BarInterface, поэтому компилятор вам поверит. Поскольку это не так, вы получаете Undefined Behavior при попытке разыменовать указатель. Обсуждение поведения скомпилированной программы в присутствии Undefined Behavior зачастую бессмысленно.

В этом случае ваш компилятор заполняет виртуальные таблицы, запись для FooInterface::Foo оказывается в том же месте, что и запись для BarInterface::Bar. В результате, когда вы вызываете bar->Bar(), компилятор просматривает виртуальную таблицу FooInterface, находит запись для FooInterface::Foo и вызывает ее. Если бы макеты классов были другими или сигнатуры функций были бы другими, последствия были бы гораздо более серьезными.

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

2
1201ProgramAlarm 23 Мар 2020 в 01:33
1
Приведение не (всегда) неопределенное поведение. Предполагая, что требование выравнивания для BarInterface не строже, чем для FooInterface, разрешено приведение (в противном случае это поведение undefined, но, учитывая, что типы в основном одинаковы, это, вероятно, не так) . Результирующий BarInterface* существует, но на самом деле не указывает на объект BarInterface. Приведение его обратно к FooInterface* приведет к успешному извлечению оригинала, и это попытка разыменовать его внутри ->, которая не определена.
 – 
HTNW
23 Мар 2020 в 01:31
Хорошая точка зрения. Я не думал о том, когда именно сработал UB.
 – 
1201ProgramAlarm
23 Мар 2020 в 01:33