Как уже упоминалось в комментариях, в отношении языка это неопределенное поведение.
Однако фактическое выбранное поведение действительно показывает, как работает внутренняя часть типичного компилятора C ++, так что он все еще может было бы интересно выяснить, почему вы получили результат, который вы сделали. Тем не менее, важно помнить, что следующее объяснение не является универсальным. Не существует жестких требований, чтобы вещи работали таким образом, и любой код, полагающийся на вещи, ведущие себя таким образом, эффективно нарушается, , даже если он работает на всех компиляторах, которые вы пробуете на .
C ++ полиморфизм обычно реализуется с использованием vtable, который представляет собой список указателей функций и может рассматриваться как скрытый указатель на член в объекте.
, поэтому
struct X
{
virtual void x() = 0;
};
struct Y {
virtual void y() = 0;
};
Примерно эквивалентно (на самом деле он не использует std::function<>
, но это делает псевдокод более разборчивым):
struct X {
struct vtable_t {
std::function<void(void*)> first_virtual_function;
};
vtable_t* vtable;
void x() {
vtable->first_virtual_function(this);
}
};
struct Y {
struct vtable_t {
std::function<void(void*)> first_virtual_function;
};
vtable_t* vtable;
void y() {
vtable->first_virtual_function(this);
}
};
Обратите внимание, что X::vtable_t
и Y::vtable_t
совпадают по совпадению по существу одинаково предмет. Если бы X
и Y
имели разные виртуальные функции, вещи не выстроились бы так аккуратно.
Еще одна важная часть головоломки состоит в том, что множественное наследование является эффективной конкатенацией:
struct XY : X, Y {
void x() override { std::cout << "X\n"; }
void y() override { std::cout << "Y\n"; }
};
// is roughly equivalent to:
struct XY {
static X::vtable vtable_for_x; // with first_virtual_function assigned to XY::x()
static Y::vtable vtable_for_y; // with first_virtual_function assigned to XY::y()
X x_base;
Y y_base;
XY() {
x_base.v_table = &vtable_for_x;
y_base.v_table = &vtable_for_y;
}
void x() { std::cout << "X\n"; }
void y() { std::cout << "Y\n"; }
};
Что подразумевает, что приведение типа с множественным наследованием к основанию - это не только вопрос изменения типа указателя, значение также должно измениться.
Только указатель X
эквивалентен указателю базового объекта, указатель Y
на самом деле является другим адресом .
X* xptr = &xy;
// is equivalent to
X* xptr = &xy->x_base;
Y* xptr = &xy;
// is equivalent to
Y* xptr = &xy->y_base;
Наконец, когда вы приведение от X
к Y
, поскольку эти типы не связаны, операция является reinterpret_cast
, поэтому, хотя указатель может быть указателем на Y
, базовый объект по-прежнему является X
.
К счастью для вас, все выстраивается в ряд:
- И X, и Y имеют указатель vtable в качестве первого объекта-члена.
- И X, и таблица V фактически эквивалентны, первый указывает на
XY::x()
, затем на XY::y()
.
Так что когда логика c вызова y()
применяется к объекту типа X
, биты просто случаются выстраиваться в очередь, чтобы вместо этого вызывать XY::x()
.