Подтвердить что ты не робот

Как компилятор генерирует код для вызовов виртуальных функций?

enter image description here

CAT *p;
...
p->speak();
...

В какой-то книге говорится, что компилятор переводит p- > speak() в:

(*p->vptr[i])(p); //i is the idx of speak in the vtbl

Мой вопрос: поскольку во время компиляции невозможно узнать реальный тип p, что означает, что невозможно знать, какие vptr или vtbl использовать. Итак, как компилятор генерирует правильный код?

[модифицировано]

Например:

void foo(CAT* c)
{
    c->speak();
    //if c point to SmallCat
    // should translate to (*c->vptr[i])(p); //use vtbl at 0x1234   
    //if c point to CAT
    // should translate to (*c->vptr[i])(p); //use vtbl at 0x5678  

    //since ps,pc all are CAT*, why does compiler can generate different code for them 
    //in compiler time?
}

...
CAT *ps,*pc;
ps = new SmallCat;  //suppose SmallCat vtbl address is 0x1234;
pc = new CAT;       //suppose CAT vtbl address is 0x5678;
...
foo(ps);
foo(pc)
...

Любые идеи? Спасибо.

4b9b3361

Ответ 1

Отсутствие вашей картинки - это стрелка от объектов CAT и SmallCAT к их соответствующим vtbls. Компилятор вводит указатель на vtbl в сам объект - его можно рассматривать как скрытую переменную-член. Вот почему сказано, что добавление первой виртуальной функции "стоит" за один указатель на объект в области памяти. Указатель на vtbl настраивается кодом в конструкторе, поэтому весь виртуальный вызов, созданный компилятором, должен сделать, чтобы перейти к его vtable во время выполнения, разыменовывает указатель на this.

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

Вот ваш пример, поясненный более подробно:

CAT *p1,*p2;
p1 = new SmallCat;  //suppose its vtbl address is 0x1234;
// The layout of SmallCat object includes a vptr as a hidden member.
// At this point, the value of this vptr is set to 0x1234.
p2 = new CAT;       //suppose its vtbl address is 0x5678;
// The layout of Cat object also includes a vptr as a hidden member.
// At this point, the value of this vptr is set to 0x5678.
(*p1->vptr[i])(p); //should use vtbl at 0x1234
// Compiler has enough information to do that, because it squirreled away 0x1234
// inside the SmallCat object at the time it was constructed.
(*p2->vptr[i])(p); //should use vtbl at 0x5678
// Same deal - the constructor saved 0x5678 inside the Cat, so we're good.

Ответ 2

что означает, что невозможно знать, какие vptr или vtbl использовать

Это правильно во время вызова метода. Но во время построения тип сконструированного объекта фактически известен, и компилятор будет генерировать код в ctor для инициализации vptr, чтобы указать на vtbl соответствующего класса. Все последующие вызовы виртуального метода вызовут метод в правом vtbl через этот vptr.

Подробнее о том, как именно эта инициализация работает с базовыми объектами (с несколькими вызовами, вызываемыми последовательно), см. этот ответ по аналогичному вопросу.

Ответ 3

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

Вы можете сказать это, используя sizeof в этом классе, и увидите, что он больше, чем вы ожидали бы на 4 или 8 байтов, в зависимости от sizeof(void*).

Компилятор также добавляет к конструктору каждого класса неявный фрагмент кода, который устанавливает vptr для указания на таблицу указателей функций (a.k.a. V-Table).

Когда экземпляр объекта создается, его тип явно упоминается.

Например: A a(1) или A* p = new B(2).

Поэтому внутри конструктора во время выполнения, vptr можно легко установить правильную V-таблицу.

В приведенном выше примере:

  • vptr для a устанавливается на V-таблицу class A.

  • В vptr из p установлено значение V-таблицы class B.

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

Вот как компилятор генерирует правильный код для виртуальной функции p->speak():

CAT *p;
...
p = new SuperCat("SaberTooth",2); // p->vptr = SuperCat_Vtable
...
p->speak(); // See pseudo assembly code below

Ax = p               // Get the address of the instance
Bx = p->vptr         // Get the address of the instance V-Table
Cx = Bx + CAT::speak // Add the number of the function in its class
Dx = *Cx             // Get the address of the appropriate function
Push Ax              // Push the address of the instance into the stack
Push Dx              // Push the address of the function into the stack
CallF                // Save some registers and jump to the beginning of the function

Компилятор использует одинаковое число (индекс) для всех speak функций в иерархии class CAT.

Вот как компилятор генерирует правильный код для не виртуальной функции p->eat():

p->eat(); // See pseudo assembly code below

Ax = p        // Get the address of the instance
Bx = CAT::eat // Get the address of the function
Push Ax       // Push the address of the instance into the stack
Push Bx       // Push the address of the function into the stack
CallF         // Save some registers and jump to the beginning of the function

Поскольку адрес функции eat известен во время компиляции, код сборки более эффективен.

И, наконец, вот как "vptr" установлено, чтобы указать правильную V-таблицу во время выполнения:

class SmallCat
{
    void* vptr; // implicitly added by the compiler
    ...         // your explicit variables
    SmallCat()
    {
        vptr = (void*)0x1234; // implicitly added by the compiler
        ...                   // Your explicit code
    }
};

Когда вы создаете экземпляр CAT* p = new SmallCat(), создается новый объект с его vptr = 0x1234

Ответ 4

Когда вы пишете это (я заменил весь код пользователя строчным):

class cat {
public:
    virtual void speak() {std::cout << "meow\n";}
    virtual void eat() {std::cout << "eat\n";}
    virtual void destructor() {std::cout << "destructor\n";}
};

Компилятор генерирует все это магически (весь код кода компилятора в верхнем регистре):

class cat;
struct CAT_VTABLE_TYPE { //here the cat vtable type
    void(*speak)(cat* this); //contains a pointer for each virtual function
    void(*eat)(cat* this);
    void(*destructor)(cat* this);
};
extern CAT_VTABLE_TYPE CAT_VTABLE; //later is a global shared copy of the vtable
class cat { //here the class you typed
private:
    CAT_VTABLE_TYPE* vptr; //but the compiler adds this magic member
public:
    cat() :vptr(&CAT_VTABLE) {} //the compiler initializes the vtable ptr
    ~cat() {vptr->destructor(this);} //redirects to the one you coded
    void speak() {vptr->speak(this);} //redirects to the one you coded
    void eat() {vptr->eat(this);} //redirects to the one you coded
};

//Here the functions you programmed
void DEFAULT_CAT_SPEAK(CAT* this) {std::cout << "meow\n";}
void DEFAULT_CAT_EAT(CAT* this) {std::cout << "eat\n";}
void DEFAULT_CAT_DESTRUCTOR(CAT* this) {std::cout << "destructor\n";}
//and the global cat vtable (shared by all cat objects)
const CAT_VTABLE_TYPE CAT_VTABLE = {
    DEFAULT_CAT_SPEAK, 
    DEFAULT_CAT_EAT, 
    DEFAULT_CAT_DESTRUCTOR};

Ну, это много, не так ли? (Я фактически немного обманывал, так как я принимаю адрес объекта до его определения, но этот способ меньше кода и менее запутанный, даже если технически он несовместим). Вы можете понять, почему они построили его на языке. И... здесь SmallCat:

class smallcat : public cat {
public:
    virtual void speak() {std::cout << "meow2\n";}
    virtual void destructor() {std::cout << "destructor2\n";}
};

и после:

class smallcat;
//here the smallcat vtable type
struct SMALLCAT_VTABLE_TYPE : public CAT_VTABLE_TYPE { 
     //contains no additional virtual functions that cat didn't have
};
extern SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE; //later is a global shared copy of the vtable
class smallcat : public cat { //here the class you typed
public:
    smallcat() :vptr(&SMALLCAT_VTABLE) {} //the compiler initializes the vtable ptr
    //The other functions already are virtual, nothing additional needed
};
//Here the functions you programmed
void DEFAULT_SMALLCAT_SPEAK(CAT* this) {std::cout << "meow2\n";}
void DEFAULT_SMALLCAT_DESTRUCTOR(CAT* this) {std::cout << "destructor2\n";}
//and the global cat vtable (shared by all cat objects)
const SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE = {
    DEFAULT_SMALLCAT_SPEAK, 
    DEFAULT_CAT_EAT, //note: eat wasn't overridden
    DEFAULT_SMALLCAT_DESTRUCTOR};

Итак, если это слишком много для чтения, компилятор создает объект VTABLE для каждого типа, который указывает на функции-члены для этого конкретного типа, а затем он вставляет указатель на этот VTABLE внутри каждого экземпляра.

Когда вы создаете объект smallcat, компилятор строит родительский объект cat, который присваивает vptr точку в CAT_VTABLE global. Сразу же после этого компилятор создает объект smallcat, который перезаписывает член vptr, чтобы он указывал на глобальный SMALLCAT_VTABLE.

Когда вы вызываете c->speak();, компилятор производит вызовы, которые он копирует cat::speak, (который выглядит как this->vptr->speak(this);). Элемент vptr может указывать на глобальный CAT_VTABLE или глобальный SMALLCAT_VTABLE, и поэтому указатель таблицы speak указывает либо на DEFAULT_CAT_SPEAK (что вы помещаете в cat::speak), либо DEFAULT_SMALLCAT_SPEAK (код, который вы разместили в smallcat::speak). Таким образом, this->vptr->speak(this); завершает вызов функции для самого производного типа, независимо от того, какой из них имеет самый производный тип.

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