С++ Будут ли работать виртуальные функции, если я перетаскиваю производный объект в его базовый тип

Рассмотрим следующее:

  • B наследует от A и переопределяет функцию печати. ​​
  • A имеет статическую функцию, которая принимает void *, отбрасывает ее в и вызывает функцию виртуальной печати. ​​
  • Если void * изначально был B, он вызывает A:: print или B:: print?

    #include <iostream>
    
    class A {
    public:
            static void w(void *p) {
    
                    A *a = reinterpret_cast<A*>(p);
                    a->print();
            }
    
            virtual void print() {
               std::cout << "A"  << std::endl;
            }
    };
    
    class B : public A {
    
    public:
            void print() {
               std::cout << "B"  << std::endl;
            }
    };
    
    int main () {
            B b;
            A::w(&b);
    }
    

Это печатает B для меня.

Кажется, что void *, который был добавлен в A, все еще знает о переопределенной функции печати B. Причина не сразу понятна.

Может кто-нибудь объяснить мне, если это поведение, на которое я могу положиться, или это просто какая-то случайность, которая работает, потому что это небольшой пример (например, как возвращение ссылки на локальную переменную не всегда будет segfault в небольших программах).

Ответ 1

Виртуальные функции обычно разрешаются неявным vtable. Это в основном массив указателей на функции для каждой виртуальной функции в иерархии классов. Компилятор добавляет его как "скрытый элемент" к вашему классу. При вызове виртуальной функции вызывается соответствующая запись в vtable.

Теперь, когда вы создаете класс типа B, он неявно имеет B-vtable, хранящуюся в объекте. Каста не влияют на эту таблицу.

Следовательно, при нажатии void * на A присутствует исходная таблица vtable (класса B), которая указывает на B::print.

Обратите внимание, что это поведение, определяемое реализацией, и стандарт ничего не гарантирует об этом. Но большинство компиляторов будут действовать следующим образом

Ответ 2

ваш код имеет undefined поведение

§ 5.2.10 Реинтерпрет-литье

7 Преобразование prvalue типа "указатель на T1" в тип "указатель на T2" (где T1 и T2 являются типами объектов, а требования к выравниванию T2 не более строгие, чем требования T1) и обратно к исходному типу дают исходное значение указателя. Результат любого другого такого преобразования указателя не указан.

Ответ 3

Прежде всего, ваш reinterpret_cast - undefined. Если вы пройдете A* до w, он будет определен.

A * p = new B;
A::w(p);
delete p;

Я предлагаю использовать static_cast<A*>(p) вместо этого, если w всегда вызывается с помощью A*.

Если у вас определенный прилив на void* и обратно, адрес памяти остается постоянным. Таким образом, a внутри вашего w будет действительным A*, если сначала передать действительный A* в w.

Вопрос о том, почему программа знает, как обращаться с вызовом, связана с механиком под названием "Виртуальная таблица".

Примечание. Это может быть различным для разных компиляторов. Я расскажу о том, как Visual Studio, похоже, справляется с этим.

Чтобы дать вам несколько приблизительную идею для простого наследования:

Компилятор выполнит в вашем коде 2 print функции: A::print (т.е. по адресу X) и B::print (, т.е. по адресу Y).

Действительный объем памяти класса, содержащего виртуальную функцию (т.е.

struct A
{
  void print (void);
  size_t x;
};
struct B : A
{
  void print (void);
  size_t y;
};

) будет несколько похож на

struct Real_A
{
  void * vtable;
  size_t x;
};
struct Real_B : Real_A
{
  size_t y;
};

Кроме того, будут существовать две так называемые виртуальные таблицы: по одному для каждого класса, содержащего виртуальные функции или имеющие базовый класс с виртуальными функциями.

Вы можете представить vtable как структуру, содержащую "реальный" адрес для каждой функции.

После компиляции компилятор создаст Vtables для каждого класса (a и B): Каждый экземпляр A будет иметь vtable = <Address of A Vtable>, тогда как каждый экземпляр B будет иметь vtable = <Address of B Vtable>.

Во время выполнения, если вызывается виртуальная функция, программа будет искать "реальный" адрес для функции из таблицы Vtable, которая хранится по адресу, который является первым элементом для каждого объекта a или B

Следующий код является нестандартным и не нормальным ... но это может дать вам идею, хотя...

#include <iostream>
struct A 
{
  virtual void print (void) { std::cout << "A called." << std::endl; }
  size_t x;
};

struct B : A 
{
  void print (void) { std::cout << "B called." << std::endl; }
};
// "Real" memory layout of A
struct Real_A
{
  void * vtable;
  size_t x_value;
};
// "Real" memory layout of B
struct Real_B : Real_A
{
  size_t y_value;
};
// "Pseudo virtual table structure for classes with 1 virtual function"
struct VT
{
  void * func_addr;
};

int main (void) 
{
  A * pa = new A;
  pa->x = 15;
  B * pb = new B;
  pb->x = 20;
  A * pa_b = new B;
  pa_b->x = 25;
  // reinterpret addrress of A and B objects as Real_A and Real_B
  Real_A& ra(*(Real_A*)pa);
  Real_B& rb(*(Real_B*)pb);
  // reinterpret addrress of B object through pointer to A as Real_B
  Real_B& rb_a(*(Real_B*)pa_b);
  // Print x_values to know whether we meet the class layout
  std::cout << "Value of ra.x_value = " << ra.x_value << std::endl;
  std::cout << "Value of rb.x_value = " << rb.x_value << std::endl;
  std::cout << "Value of rb.x_value = " << rb_a.x_value << std::endl;
  // Print vtable addresses
  std::cout << "VT of A through A*: " << ra.vtable << std::endl;
  std::cout << "VT of B through B*: " << rb.vtable << std::endl;
  std::cout << "VT of B through A*: " << rb_a.vtable << std::endl;
  // Reinterpret memory pointed to by the vtable address as VT objects
  VT& va(*(VT*)ra.vtable);
  VT& vb(*(VT*)rb.vtable);
  VT& vb_a(*(VT*)rb_a.vtable);
  // Print addresses of functions in the vtable
  std::cout << "FA of A through A*: " << va.func_addr << std::endl;
  std::cout << "FA of B through B*: " << vb.func_addr << std::endl;
  std::cout << "FA of B through A*: " << vb_a.func_addr << std::endl;

  delete pa;
  delete pb;
  delete pa_b;

  return 0;
}

Выход Visual Studio 2013:

 Value of ra.x_value = 15
Value of rb.x_value = 20
Value of rb.x_value = 25
VT of A through A*: 00D9DC80
VT of B through B*: 00D9DCA0
VT of B through A*: 00D9DCA0
FA of A through A*: 00D914B0
FA of B through B*: 00D914AB
FA of B through A*: 00D914AB

Выход gcc-4.8.1:

Value of ra.x_value = 15
Value of rb.x_value = 20
Value of rb.x_value = 25
VT of A through A*: 0x8048f38
VT of B through B*: 0x8048f48
VT of B through A*: 0x8048f48
FA of A through A*: 0x8048d40
FA of B through B*: 0x8048cc0
FA of B through A*: 0x8048cc0

https://ideone.com/iKyBB3

Примечание. Независимо от того, обращаетесь ли вы к объекту B через A* или B*, вы сначала найдете тот же самый адрес vtable, и вы найдете тот же адрес, что и в таблице vtable.

Ответ 4

reinterpret_cast не может работать, если вы рассматриваете, как многократное наследование реализовано на С++.

В принципе, при переходе между связанными типами, с множественным наследованием, приведение может включать добавление смещения. Следовательно, не зная тип источника и назначения, компилятор не может исправить правильные инструкции.

Таким образом, повторные интерпретации отбрасываются, по крайней мере, для этой утилиты, поэтому они определяются как undefined.

Опасная часть здесь, даже если вы не выполняете множественное наследование, заключается в том, что современные компиляторы начинают интерпретировать это поведение "undefined", что означает, что они могут оптимизировать предмет, содержащий блок и т.д. Это почти наверняка справедливо по стандарту С++ (undefined означает точно, что все в порядке), но может быть сюрпризом для разработчика, который, как правило, понимает "undefined", поскольку "вывод кода может вообще не работать".