Защо се изисква виртуална таблица само в случай на виртуални функции?

От 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, което може да прави по време на компилиране.

Въпреки че виртуалните функции използват същия синтаксис за извикване като невиртуалните членски функции, вие всъщност дереферирате указателя на обекта; конкретно, вие дереферирате указателя към виртуалната таблица на класа на обекта.

person pmdj    schedule 02.03.2012