Виртуални таблици и оформление на паметта в множествено виртуално наследяване

Помислете за следната йерархия:

struct A {
   int a; 
   A() { f(0); }
   A(int i) { f(i); }
   virtual void f(int i) { cout << i; }
};
struct B1 : virtual A {
   int b1;
   B1(int i) : A(i) { f(i); }
   virtual void f(int i) { cout << i+10; }
};
struct B2 : virtual A {
   int b2;
   B2(int i) : A(i) { f(i); }
   virtual void f(int i) { cout << i+20; }
};
struct C : B1, virtual B2 {
   int c;
   C() : B1(6),B2(3),A(1){}
   virtual void f(int i) { cout << i+30; }
};
  1. Какво е точното оформление на паметта на C екземпляр? Колко vptrs съдържа, къде точно е поставен всеки от тях? Кои от виртуалните таблици се споделят с виртуалната таблица на C? Какво точно съдържа всяка виртуална таблица?

    Ето как разбирам оформлението:

    ----------------------------------------------------------------
    |vptr1 | AptrOfB1 | b1 | B2ptr | c | vptr2 | AptrOfB2 | b2 | a |
    ----------------------------------------------------------------
    

    където AptrOfBx е указателят към A екземпляр, който Bx съдържа (тъй като наследяването е виртуално).
    Това правилно ли е? Към кои функции сочи vptr1? Към кои функции сочи vptr2?

  2. Като се има предвид следният код

    C* c = new C();
    dynamic_cast<B1*>(c)->f(3);
    static_cast<B2*>(c)->f(3);
    reinterpret_cast<B2*>(c)->f(3);
    

    Защо всички обаждания до f отпечатват 33?


person JeB    schedule 22.07.2012    source източник
comment
Това домашно ли е, или любопитство?   -  person templatetypedef    schedule 23.07.2012
comment
Всъщност това е изпит. Но съм сигурен, че ако най-накрая разбера как работят нещата в този пример, мога да разбера всичко, което е свързано с множествено наследяване и виртуално наследяване.   -  person JeB    schedule 23.07.2012
comment
Можете лесно да откриете началото на всеки родителски подобект по следния начин: C foo; intptr_t offsetB1 = (intptr_t)(B1*)&foo - (intptr_t)&foo;, началите на другите бази могат да бъдат получени аналогично. Също така, изчисляването на sizeof на всички класове трябва да ви даде още една добра следа.   -  person cmaster - reinstate monica    schedule 18.07.2014


Отговори (2)


Виртуалните бази са много различни от обикновените бази. Не забравяйте, че „виртуален“ означава „определен по време на изпълнение“ -- следователно целият основен подобект трябва да бъде определен по време на изпълнение.

Представете си, че получавате B & x препратка и имате задачата да намерите члена A::a. Ако наследството беше реално, тогава B има суперклас A и по този начин B-обектът, който преглеждате през x, има A-подобект, в който можете да намерите вашия член A::a. Ако най-производният обект на x има множество бази от тип A, тогава можете да видите само това конкретно копие, което е подобект на B.

Но ако наследството е виртуално, нищо от това няма смисъл. Не знаем кой A-подобект ни трябва -- тази информация просто не съществува по време на компилиране. Можем да имаме работа с действителен B-обект като B y; B & x = y;, или с C-обект като C z; B & x = z;, или нещо съвсем различно, което произлиза виртуално от A много повече пъти. Единственият начин да разберете е да намерите действителната база A по време на изпълнение.

Това може да се реализира с още едно ниво на индиректност по време на изпълнение. (Обърнете внимание, че това е напълно успоредно на начина, по който виртуалните функции се изпълняват с едно допълнително ниво на индиректност по време на изпълнение в сравнение с невиртуалните функции.) Вместо да има указател към vtable или основен подобект, едно решение е за съхраняване на указател към указател към действителния основен подобект. Това понякога се нарича "тунк" или "батут".

Така че действителният обект C z; може да изглежда по следния начин. Действителното подреждане в паметта зависи от компилатора и е маловажно, а аз съм потиснал vtables.

+-+------++-+------++-----++-----+
|T|  B1  ||T|  B2  ||  C  ||  A  |
+-+------++-+------++-----++-----+
 |         |                 |
 V         V                 ^
 |         |       +-Thunk-+ |
 +--->>----+-->>---|     ->>-+
                   +-------+

По този начин, без значение дали имате B1& или B2&, вие първо търсите thunk, а той на свой ред ви казва къде да намерите действителния основен подобект. Това също обяснява защо не можете да извършите статично прехвърляне от A& към който и да е от производните типове: тази информация просто не съществува по време на компилиране.

За по-задълбочено обяснение погледнете тази хубава статия. (В това описание thunk е част от vtable на C и виртуалното наследяване винаги налага поддръжката на vtables, дори ако никъде няма виртуални функции.)

person Kerrek SB    schedule 22.07.2012
comment
Благодаря ви за страхотния отговор. Както знам, thunk е част от виртуалната таблица. т.е. ако не се нуждаете от отместване, за да разберете, че функцията на обекта работи, нямате нужда от thunk. Ако имате нужда от отместване, така че в съответното поле на vtable има указател към thunk, който съдържа отместването и указателя към действителната функция. Така че ми е интересно да знам как в моя пример изглеждат vtables. т.е. към кои от функциите сочат и кои от функциите се сочат през thunks. - person JeB; 23.07.2012
comment
Освен това съм много изненадан, че всички отливки (статични, динамични, преинтерпретирани) ме насочват към една специфична функция C::f. Много е странно. Можете ли да обясните как (в този пример) работи всеки един от тях? Също така, прочетох много статии за това и статията, към която поставихте връзка, беше една от първите статии, които прочетох. Все още не ми помага да разбера какво се случва тук. - person JeB; 23.07.2012
comment
@user1544364 всички преобразувания () насочват ме към една конкретна функция Не. Тези преобразувания връщат указател на обект, а не функция. - person curiousguy; 26.07.2012
comment
@user1544364 thunk, който съдържа отместването и указателя към действителната функция. Не. Thunk не съдържа данни, thunk се състои от изпълним код. Thunk е просто оптимизирана функция. - person curiousguy; 26.07.2012
comment
Връзката към хубавата статия не работи, но намерих резервното копие: cs.nyu.edu/courses/fall16/CSCI-UA.0470-001/slides/ - person tuket; 09.05.2019

Направих малко вашия код, както следва:

#include <stdio.h>
#include <stdint.h>

struct A {
   int a; 
   A() : a(32) { f(0); }
   A(int i) : a(32) { f(i); }
   virtual void f(int i) { printf("%d\n", i); }
};

struct B1 : virtual A {
   int b1;
   B1(int i) : A(i), b1(33) { f(i); }
   virtual void f(int i) { printf("%d\n", i+10); }
};

struct B2 : virtual A {
   int b2;
   B2(int i) : A(i), b2(34) { f(i); }
   virtual void f(int i) { printf("%d\n", i+20); }
};

struct C : B1, virtual B2 {
   int c;
   C() : B1(6),B2(3),A(1), c(35) {}
   virtual void f(int i) { printf("%d\n", i+30); }
};

int main() {
    C foo;
    intptr_t address = (intptr_t)&foo;
    printf("offset A = %ld, sizeof A = %ld\n", (intptr_t)(A*)&foo - address, sizeof(A));
    printf("offset B1 = %ld, sizeof B1 = %ld\n", (intptr_t)(B1*)&foo - address, sizeof(B1));
    printf("offset B2 = %ld, sizeof B2 = %ld\n", (intptr_t)(B2*)&foo - address, sizeof(B2));
    printf("offset C = %ld, sizeof C = %ld\n", (intptr_t)(C*)&foo - address, sizeof(C));
    unsigned char* data = (unsigned char*)address;
    for(int offset = 0; offset < sizeof(C); offset++) {
        if(!(offset & 7)) printf("| ");
        printf("%02x ", (int)data[offset]);
    }
    printf("\n");
}

Както виждате, това отпечатва доста допълнителна информация, която ни позволява да изведем оформлението на паметта. Резултатът на моята машина (64-битов Linux, малък ред на байтовете) е следният:

1
23
16
offset A = 16, sizeof A = 16
offset B1 = 0, sizeof B1 = 32
offset B2 = 32, sizeof B2 = 32
offset C = 0, sizeof C = 48
| 00 0d 40 00 00 00 00 00 | 21 00 00 00 23 00 00 00 | 20 0d 40 00 00 00 00 00 | 20 00 00 00 00 00 00 00 | 48 0d 40 00 00 00 00 00 | 22 00 00 00 00 00 00 00 

И така, можем да опишем оформлението, както следва:

+--------+----+----+--------+----+----+--------+----+----+
|  vptr  | b1 | c  |  vptr  | a  | xx |  vptr  | b2 | xx |
+--------+----+----+--------+----+----+--------+----+----+

Тук xx означава подплата. Обърнете внимание как компилаторът е поставил променливата c в подложката на своята невиртуална база. Имайте предвид също, че и трите v-указателя са различни, това позволява на програмата да изведе правилните позиции на всички виртуални бази.

person cmaster - reinstate monica    schedule 17.07.2014