Почему виртуальная таблица требуется только в случае виртуальных функций?

Из http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/, такой код, как

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};

создает виртуальную таблицу, аналогичную http://www.learncpp.com/images/CppTutorial/Section12/VTable.gif: введите здесь описание изображения

Виртуальная таблица, как указано выше, имеет смысл. В конце концов, объектам нужен способ вызова функций, и для их поиска нужно использовать указатели на функции.


Чего я не понимаю, так это почему это требуется только в случае использования виртуальных функций? Я определенно что-то упускаю, поскольку виртуальная таблица напрямую не зависит от виртуальных функций.

Например, если используемый код

class Base
{
public:
    void function1() {};
    void function2() {};
};

...

Base b;
b.function1();

и нет виртуальной таблицы (это означает, что нет указателя на то, где находится функция), как будет разрешаться вызов b.function1()?


Или у нас и в этом случае есть таблица, просто она не называется виртуальной таблицей? В таком случае возник бы вопрос, зачем нужна таблица нового типа для виртуальных функций?


person Lazer    schedule 02.03.2012    source источник
comment
Пожалуйста, поймите, что vtable — это деталь реализации. В языке нет требований к форме, функции или даже существованию виртуальной таблицы. Ваш компилятор может использовать его, и он может выглядеть как ваша картинка. Но опять же, может и нет.   -  person Robᵩ    schedule 03.03.2012
comment
Вы также получите vptr при виртуальном выводе. Кроме этого, зачем создавать vptr/vtable, если он вам не нужен?   -  person PlasmaHH    schedule 03.03.2012


Ответы (2)


[Если] нет виртуальной таблицы (это означает, что нет указателя на то, где находится функция), как будет разрешаться вызов b.function1()?

Внутри компилятора есть «указатель», поскольку он анализирует ваш код. Компилятор - это то, что решает, где будет сгенерирована функция, поэтому он знает, как должны разрешаться вызовы указанной функции. Вместе с компоновщиком все это аккуратно происходит в процессе сборки.

Единственная причина, по которой это не работает для virtual функций, заключается в том, что какая функция, которую вы вызываете, зависит от типа, известного только во время выполнения; на самом деле те же самые указатели функций дословно присутствуют в виртуальной таблице, записанной туда компилятором. Просто в этом случае есть несколько вариантов для выбора, и они не могут быть выбраны до тех пор, пока долго (читай: потенциально месяцы или даже годы!) после того, как компилятор вообще перестанет участвовать.

person Lightness Races in Orbit    schedule 02.03.2012

Уже есть хороший ответ, но я попробую немного проще (хотя и дольше):

Подумайте о невиртуальном методе формы

class A
{
public:
  int fn(int arg1);
};

как эквивалент свободной функции вида:

int fn(A* me, int arg1); // overload A

где me соответствует указателю this внутри версии метода.

Если у вас теперь есть подкласс:

class B : public A
{
public:
  int fn(int arg1);
};

это эквивалентно такой бесплатной функции:

int fn(B* me, int arg1); // overload B

Обратите внимание, что тип первого аргумента отличается от типа объявленной нами ранее свободной функции — функция перегружается по типу первого аргумента.

Если теперь у вас есть код, вызывающий fn(), он выберет перегрузку на основе статического типа (типа времени компиляции) первого аргумента:

A* p;
B* q;
// ...
// assign valid pointer values to p and q
// ...
int a = fn(p, 0); // will call overload A
int b = fn(q, 0); // will call overload B

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

Теперь, когда я сказал думать о версии метода как эквивалентной версии свободной функции, вы обнаружите, что на уровне языка ассемблера они эквивалентны. Единственным отличием будет так называемое искаженное имя, которое кодирует тип в имени скомпилированной функции и отличает перегруженные функции. Дело в том, что вы вызываете методы через p->fn(0), то есть с первым аргументом перед именем метода чисто синтаксический сахар — вы на самом деле не разыменовываете указатель p в примере, хотя это выглядит так. Вы просто передаете p в качестве неявного аргумента this. Итак, продолжая приведенный выше пример,

p->fn(0); // will always call A::fn()
q->fn(0); // will always call B::fn()

поскольку fn не является виртуальным методом, это означает, что компилятор отправляет указатель this на тип static указателя, что он может сделать во время компиляции.

Хотя виртуальные функции используют тот же синтаксис вызова, что и невиртуальные функции-члены, вы фактически разыменовываете указатель объекта; в частности, вы разыменовываете указатель на виртуальную таблицу класса объекта.

person pmdj    schedule 02.03.2012