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 не "обнаруживается". Как это все работает на самом деле?
Здравствуйте, 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, и к таблице виртуальных методов обращаться не будет,
а вызовет метод напрямую.
Здравствуйте, 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 нет)
Здравствуйте, straw dog, Вы писали:
SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается".
Никто ничего не урезает, таблица остаётся. Просто, так как это указатель на A, то вызывать метод, неизвестный для A, нельзя. Это единственная причина.
А если Вы уверены, что это объект типа B, то можно проконвертировать указатель через static_cast<> к типу указателя на B и вызвать таки этот baz(). При этом никто обратно таблицу не расширяет, она остаётся такой же, просто компилятор получает знание, как вызвать baz.
А если не уверены, что это объект типа B, то можно проконвертировать указатель через dynamic_cast<> к типу указателя на B, и проверить, что если не nullptr, то это таки был B.
Но все такие castʼы к потомкам считаются не лучшим методом, и рекомендуется не строить логику на их применении, это обычно затычки для спецслучаев.
Здравствуйте, straw dog, Вы писали:
SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?
А vеable тут совсем и ни при чём...
Тип указателя какой ? A*. Есть у A метод baz() ?
Нет.
Всё, до свидания.
Здравствуйте, straw dog, Вы писали:
SD>Хочу разобраться в следующем вопросе. SD>... SD>Далее как я это себе представляю: при кастинге B к А, vtable берется от B, но "урезается" до размера vtable класса А, поэтому при компиляции индекса метода baz в vtable не "обнаруживается". Как это все работает на самом деле?
Прежде чем вникать, как это устроено, нужно понять, как этим правильно пользоваться. Виртуальные функции позволяют по-разному реализовать (определить) один и тот же метод в разных производных классах, при этом этот метод должен быть объявлен в их общем базовом классе. А как именно устроена виртуальная таблица — это уже детали реализации и в каждом компиляторе это сделано по-своему. В стандарте языка C++ даже нет терминов таких как "virtual table" или "vtable" — описан лишь синтаксис и производимый эффект.
--
Не можешь достичь желаемого — пожелай достигнутого.
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). И далее вызвать функцию, адрес которой находится по этому смещению.