Вопрос про VPTR и VTABLE
От: straw dog  
Дата: 16.04.17 11:32
Оценка:
Привет,
Хочу разобраться в следующем вопросе.

class A
{
public:
  virtual ~A() {}
 
  virtual void foo() const { cout << "A::foo()" << endl; }
  virtual void bar() const { cout << "A::bar()" << endl; }
};

class B : public A
{
public:
  virtual ~B() {}
 
  virtual void foo() const { cout << "B::foo()" << endl; }
  virtual void bar() const { cout << "B::bar()" << endl; }
  virtual void baz() const { cout << "B::baz()" << endl; }
};

A * pA = new B;
pA->baz(); // <-- тут естественно ошибка при компиляции


Вопрос: Понятно, что на выделенной строке будет ошибка при компиляции, хочу понять в деталях как этот механизм работает. При создании объекта класса A вызовется конструктор А, который запишет в vptr адрес vtable класса А. Далее конструктор B перезапишет vptr объекта адресом vtable класса B. vptr у объекта один и тут пока все просто. Далее при вызове виртуальных функwий через pA используется vptr, указывающий на vtable класса B, в котором уже есть адрес метода baz с индексом 2, т.е. формально в используемой vtable все же есть адрес нужной функции.
Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?
c++
Re: Вопрос про VPTR и VTABLE
От: Zhendos  
Дата: 16.04.17 11:53
Оценка: +2
Здравствуйте, straw dog, Вы писали:

SD>Вопрос: Понятно, что на выделенной строке будет ошибка при компиляции, хочу понять в деталях как этот механизм работает. При создании объекта класса A вызовется конструктор А, который запишет в vptr адрес vtable класса А. Далее конструктор B перезапишет vptr объекта адресом vtable класса B. vptr у объекта один и тут пока все просто. Далее при вызове виртуальных функwий через pA используется vptr, указывающий на vtable класса B, в котором уже есть адрес метода baz с индексом 2, т.е. формально в используемой vtable все же есть адрес нужной функции.

SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?

ИМХО, стадии синтаксической проверки осуществляется намного раньше
работы с vtable, т.е. в данном случае компилятор имеет информацию о типе "класса A",
и что у него нет метода с такой сигнатурой и просто сообщает об этом,
никаких vtable еще не сгенерировано, ничего не урезается и т.д..


Если хочется посмотреть как vtable устроены в действительности,
можно использовать ключ `-fdump-class-hierarchy`,
для вашего кода он сгенерирует следующую информацию:

Vtable for A
A::_ZTV1A: 6u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::~A
24    (int (*)(...))A::~A
32    (int (*)(...))A::foo
40    (int (*)(...))A::bar

Class A
   size=8 align=8
   base size=8 base align=8
A (0x0x7f8c3cc0e7e0) 0 nearly-empty
    vptr=((& A::_ZTV1A) + 16u)

Vtable for B
B::_ZTV1B: 7u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1B)
16    (int (*)(...))B::~B
24    (int (*)(...))B::~B
32    (int (*)(...))B::foo
40    (int (*)(...))B::bar
48    (int (*)(...))B::baz

Class B
   size=8 align=8
   base size=8 base align=8
B (0x0x7f8c3c91cb60) 0 nearly-empty
    vptr=((& B::_ZTV1B) + 16u)
  A (0x0x7f8c3cc0e840) 0 nearly-empty
      primary-for B (0x0x7f8c3c91cb60)


а что будет в конечном коде (если вы исправите ошибку) неизвестно.
Современные компиляторы умеют девиртуализацию, поэтому возможно компилятор поймет
что это на самом деле класс B, и к таблице виртуальных методов обращаться не будет,
а вызовет метод напрямую.
Re: Вопрос про VPTR и VTABLE
От: Alexander G Украина  
Дата: 16.04.17 11:54
Оценка:
Здравствуйте, straw dog, Вы писали:

SD>Вопрос: Понятно, что на выделенной строке будет ошибка при компиляции, хочу понять в деталях как этот механизм работает. При создании объекта класса A вызовется конструктор А, который запишет в vptr адрес vtable класса А. Далее конструктор B перезапишет vptr объекта адресом vtable класса B. vptr у объекта один и тут пока все просто. Далее при вызове виртуальных функwий через pA используется vptr, указывающий на vtable класса B, в котором уже есть адрес метода baz с индексом 2, т.е. формально в используемой vtable все же есть адрес нужной функции.

SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?

При единичном наследовании — да.
В некоторых случаях множественного наследования класс наследует более одной vtable, и нет смысла, чтобы его новые методы попали во все таблицы.

class A1
{
public:
  virtual ~A1() {}
 
  virtual void foo() const { cout << "A1::foo()" << endl; }
};

class A2
{
public:
  virtual ~A2() {}
 
  virtual void bar() const { cout << "A2::bar()" << endl; }
};


class B : public A1, public A2
{
public:
  virtual ~B() {}
 
  virtual void foo() const { cout << "B::foo()" << endl; }
  virtual void bar() const { cout << "B::bar()" << endl; }
  virtual void baz() const { cout << "B::baz()" << endl; }
};

B * pB = new B;
A1 * pA1 = pB;
A2 * pA2 = pB;
// pA1->baz(); // <-- baz мог попасть в эту таблицу..
// pA2->baz(); // <-- ...или в эту



(disclaimer: когда мы говорим vtable, мы говорим о деталях реализаций, в стандарте vtable нет)
Русский военный корабль идёт ко дну!
Re: Вопрос про VPTR и VTABLE
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 16.04.17 14:11
Оценка:
Здравствуйте, straw dog, Вы писали:

SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается".


Никто ничего не урезает, таблица остаётся. Просто, так как это указатель на A, то вызывать метод, неизвестный для A, нельзя. Это единственная причина.

А если Вы уверены, что это объект типа B, то можно проконвертировать указатель через static_cast<> к типу указателя на B и вызвать таки этот baz(). При этом никто обратно таблицу не расширяет, она остаётся такой же, просто компилятор получает знание, как вызвать baz.
А если не уверены, что это объект типа B, то можно проконвертировать указатель через dynamic_cast<> к типу указателя на B, и проверить, что если не nullptr, то это таки был B.

Но все такие castʼы к потомкам считаются не лучшим методом, и рекомендуется не строить логику на их применении, это обычно затычки для спецслучаев.
The God is real, unless declared integer.
Re: Вопрос про VPTR и VTABLE
От: MasterZiv СССР  
Дата: 19.04.17 06:14
Оценка:
Здравствуйте, straw dog, Вы писали:

SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?


А vеable тут совсем и ни при чём...
Тип указателя какой ? A*. Есть у A метод baz() ?
Нет.
Всё, до свидания.
Re: Вопрос про VPTR и VTABLE
От: rg45 СССР  
Дата: 19.04.17 06:41
Оценка:
Здравствуйте, straw dog, Вы писали:

SD>Хочу разобраться в следующем вопросе.

SD>...
SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?

Прежде чем вникать, как это устроено, нужно понять, как этим правильно пользоваться. Виртуальные функции позволяют по-разному реализовать (определить) один и тот же метод в разных производных классах, при этом этот метод должен быть объявлен в их общем базовом классе. А как именно устроена виртуальная таблица — это уже детали реализации и в каждом компиляторе это сделано по-своему. В стандарте языка C++ даже нет терминов таких как "virtual table" или "vtable" — описан лишь синтаксис и производимый эффект.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 19.04.2017 6:43 rg45 . Предыдущая версия . Еще …
Отредактировано 19.04.2017 6:42 rg45 . Предыдущая версия .
Re: Вопрос про VPTR и VTABLE
От: alzt  
Дата: 02.05.17 19:48
Оценка:
Здравствуйте, straw dog, Вы писали:

SD>
SD>A * pA = new B;
pA->>baz(); // <-- тут естественно ошибка при компиляции
SD>


Статический тип pA — класс A, у него нет метода baz, поэтому этот код не должен компилироваться. Виртуальные таблицы тут ни при чём.

SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?


Добавим метод baz к A для компиляции.
При вызове pA->baz компилятор сгенерирует код:
взять адрес виртуальной таблицы (это будет адрес таблицы B), и сделать некое константное смещение (а это будет B::baz). И далее вызвать функцию, адрес которой находится по этому смещению.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.