XY наследует от X и Y. приведение XY * к X *, затем к Y *, затем вызов функции Y приводит к вызову функции X - PullRequest
1 голос
/ 01 мая 2020
#include <iostream>

struct X
{
    virtual void x() = 0;
};

struct Y
{
    virtual void y() = 0;
};

struct XY : X, Y
{
    void x() override { std::cout << "X\n"; }
    void y() override { std::cout << "Y\n"; }
};

int main()
{
    XY xy;

    X* xptr = &xy;

    Y* yptr = (Y*)xptr;

    yptr->y(); //prints "X"....

    ((Y*)((X*)(&xy)))->y(); // prints "Y"....
}

Вывод:

X
Y

Может кто-нибудь объяснить в деталях, почему это происходит? Почему первый звонок печатает X, а также почему два звонка отличаются друг от друга?

Ответы [ 2 ]

3 голосов
/ 01 мая 2020

Y* yptr = (Y*)xptr; делает reinterpret_cast

С Явное преобразование типов ( new_type ) expression:

Когда выражение приведения в стиле C имеет вид При обнаружении компилятор пытается интерпретировать его как следующие выражения приведения в следующем порядке:

a) const_cast<new_type>(expression);
b) static_cast<new_type>(expression) с расширениями: указатель или ссылка на производный класс дополнительно разрешено приводить к указателю или ссылаться на однозначный базовый класс (и наоборот), даже если базовый класс недоступен (то есть это приведение игнорирует спецификатор частного наследования). То же самое относится и к приведению указателя на член к указателю на однозначную не виртуальную базу;
c) static_cast (с расширениями), за которыми следуют const_cast;
d) reinterpret_cast<new_type>(expression);
e) reinterpret_cast с последующим const_cast.

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

a , b и c не будут работать, поэтому он приземлится на d . Правильное приведение, dynamic_cast, даже не учитывается, когда вы выполняете приведение в стиле C, поэтому у вас все еще есть указатель на X часть XY, которую вы разыменовываете глазами Y когда вы делаете yptr->y(). Это делает вашу программу неопределенным поведением .

Никогда не используйте C стиль приведения. Лучше быть явным, чтобы вы знали, что получаете правильное приведение:

Y* yptr = dynamic_cast<Y*>(xptr);
3 голосов
/ 01 мая 2020

Как уже упоминалось в комментариях, в отношении языка это неопределенное поведение.

Однако фактическое выбранное поведение действительно показывает, как работает внутренняя часть типичного компилятора 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().

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...