Макет объекта в случае виртуальных функций и множественного наследования

StackOverflow https://stackoverflow.com/questions/1321062

Вопрос

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

  1. Какова точная макет памяти объекта класса C.
  2. Записи виртуальных таблиц для класса C.
  3. Размеры (как возвращается по размеру) объекта классов A, B и C. (8, 8, 16 ??)
  4. Что, если виртуальное наследство используется. Конечно, должны быть затронуты размеры и виртуальные записи таблицы?

Пример кода:

class A {  
  public:   
    virtual int funA();     
  private:  
    int a;  
};

class B {  
  public:  
    virtual int funB();  
  private:  
    int b;  
};  

class C : public A, public B {  
  private:  
    int c;  
};   

Спасибо!

Это было полезно?

Решение

Макет памяти и макет VTable зависят от вашего компилятора. Например, используя мой GCC, они выглядят так:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

Обратите внимание, что Sizeof (int) и пространство, необходимое для указателя VTable, также могут варьироваться от компилятора до компилятора и платформы до платформы. Причина, по которой sizeof (c) == 20, а не 16, заключается в том, что GCC дает ему 8 байтов для субобъекта, 8 байтов для субобъекта B и 4 байта для его члена int c.

Vtable for C
C::_ZTV1C: 6u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI1C)
8     A::funA
12    (int (*)(...))-0x00000000000000008
16    (int (*)(...))(& _ZTI1C)
20    B::funB

Class C
   size=20 align=4
   base size=20 base align=4
C (0x40bd5e00) 0
    vptr=((& C::_ZTV1C) + 8u)
  A (0x40bd6080) 0
      primary-for C (0x40bd5e00)
  B (0x40bd60c0) 8
      vptr=((& C::_ZTV1C) + 20u)

Использование виртуального наследования

class C : public virtual A, public virtual B

макет меняется на

Vtable for C
C::_ZTV1C: 12u entries
0     16u
4     8u
8     (int (*)(...))0
12    (int (*)(...))(& _ZTI1C)
16    0u
20    (int (*)(...))-0x00000000000000008
24    (int (*)(...))(& _ZTI1C)
28    A::funA
32    0u
36    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
44    B::funB

VTT for C
C::_ZTT1C: 3u entries
0     ((& C::_ZTV1C) + 16u)
4     ((& C::_ZTV1C) + 28u)
8     ((& C::_ZTV1C) + 44u)

Class C
   size=24 align=4
   base size=8 base align=4
C (0x40bd5e00) 0
    vptridx=0u vptr=((& C::_ZTV1C) + 16u)
  A (0x40bd6080) 8 virtual
      vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u)
  B (0x40bd60c0) 16 virtual
      vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)

Используя GCC, вы можете добавить -fdump-class-hierarchy Чтобы получить эту информацию.

Другие советы

Я буду ожидать с множественным наследством, что ваш указатель может измениться при применении на (обычно не первый) подкласс. То, о чем вы должны знать при отладке и ответе на вопросы интервью.

Во -первых, полиморфный класс имеет по крайней мере одну виртуальную функцию, поэтому он имеет VPTR:

struct A {
    virtual void foo();
};

Скомпилируется в:

struct A__vtable { // vtable for objects of declared type A
    void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};

void A__foo (A *__this); // A::foo ()

// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
    /*foo__ptr =*/ A__foo
};

struct A {
    A__vtable const *__vptr; // ptr to const not const ptr
                             // vptr is modified at runtime
};

// default constructor for class A (implicitly declared)
void A__ctor (A *__that) { 
    __that->__vptr = &A__real;
}

Замечание: C ++ может быть составлен на другой язык высокого уровня, например C (как это сделал CFRNT) или даже в подмножество C ++ (здесь C ++ без virtual) я кладу __ в компиляторе сгенерировали имена.

Обратите внимание, что это упрощенный модель, где RTTI не поддерживается; Реальные компиляторы добавят данные в VTable, чтобы поддержать typeid.

Теперь простой полученный класс:

struct Der : A {
    override void foo();
    virtual void bar();
};

Необратимые (*) базовые субобъекты являются субобъектами, такими как субобъекты участников, но в то время как субобъекты членов являются полными объектами, т.е. Их реальный (динамический) тип - их объявленный тип, базовые субобъекты не являются полными, а их реальное изменение во время строительства.

(*) Виртуальные основания очень разные, так как виртуальные функции членов отличаются от не виртуальных участников

struct Der__vtable { // vtable for objects of declared type Der
    A__vtable __primary_base; // first position
    void (*bar__ptr) (Der *__this); 
};

// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()

// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()

// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = { 
    { /*foo__ptr =*/ Der__foo },
    /*foo__ptr =*/ Der__bar
};

struct Der { // no additional vptr
    A __primary_base; // first position
};

Здесь «первая позиция» означает, что участник должен быть первым (другие участники могут быть переупорядочены): они расположены в Offset Zero, чтобы мы могли reinterpret_cast указатели, типы совместимы; При нелеровом смещении мы должны были бы сделать настройки указателя с арифметикой на char*.

Отсутствие корректировки может не показаться большим сроком сгенерированного кода (только некоторые добавляют немедленные инструкции ASM), но это значит гораздо больше, это означает, что такие указатели можно рассматривать как наличие различных типов: объект типа A__vtable* может содержать указатель на Der__vtable и рассматриваться как либо Der__vtable* или A__vtable*. Анкет Тот же объект указателя служит указателем на A__vtable В функциях, касающихся объектов типа A и как указатель на Der__vtable В функциях, касающихся объектов типа Der.

// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) { 
    A__ctor (reinterpret_cast<A*> (__this));
    __this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}

Вы видите, что динамический тип, определяемый VPTR, изменения во время конструкции, когда мы назначаем новое значение VPTR (в данном конкретном случае призыв к конструктору базового класса не делает ничего полезного и может быть оптимизировано, но он является ' t случай с не тривиальными конструкторами).

С множественным наследством:

struct C : A, B {};

А C экземпляр будет содержать A и B, как это:

struct C {
    A base__A; // primary base
    B base__B;
};

Обратите внимание, что только один из этих субобъектов базового класса может иметь привилегию сидеть в Offset Zero; Это важно во многих отношениях:

  • Преобразование указателей в другие базовые классы (Vaccasts) потребует корректировки; И наоборот, вспомогания нуждаются в противоположных корректировках;

  • Это подразумевает, что при выполнении виртуального звонка с указателем базового класса, this имеет правильное значение для записи в полученном переопределении класса.

Итак, следующий код:

void B::printaddr() {
    printf ("%p", this);
}

void C::printaddr () { // overrides B::printaddr()
    printf ("%p", this);
}

можно собрать в

void B__printaddr (B *__this) {
    printf ("%p", __this);
}

// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
    printf ("%p", __this);
}

// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
    C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}

Мы видим C__B__printaddr объявленный тип и семантика совместимы с B__printaddr, поэтому мы можем использовать &C__B__printaddr в Vtable B; C__printaddr не совместим, но может использоваться для вызовов с участием C объекты или классы, полученные из C.

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

Не виртуальный базовый класс похож на объект -член, где мы можем уточнить поведение через переопределение (также мы можем получить доступ к защищенным членам). Для внешнего мира наследование A в Der подразумевает, что неявные полученные преобразования будут существовать для указателей, что A& может быть связан с Der LVALUE и т. Д. Для получения дополнительных уроков (полученных из Der), это также означает, что виртуальные функции A унаследованы в Der: Виртуальные функции в A может быть переопределен в дальнейших полученных классах.

Когда класс получен дальше, скажем Der2 получено из Der, неявные преобразования указатели типа Der2* к A* семантически выполняется на шаге: во -первых, преобразование в Der* проверяется (контроль доступа к отношению к наследству Der2 из Der проверяется с обычными правилами общественного/защищенного/частного/друга), а затем контроль доступа Der к A. Анкет Невозможное соотношение наследства не может быть уточнено или переопределено в производных классах.

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

При выполнении не виртуального, повторяющегося, множественное наследство:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };

Есть только два Top::i Субобъекты в Bottom (Left::i а также Right::i), как и с объектами участников:

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }

Никто не удивлен, что есть два int субмесители (l.t.i а также r.t.i).

С виртуальными функциями:

struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)

это означает, что есть две разные (не связанные) виртуальные функции, вызванные foo, с различными записями VTable (оба, поскольку они имеют одинаковую подпись, они могут иметь общий переопределение).

Семантика не виртуальных базовых классов следует из того факта, что основное, не виртуальное наследство является исключительным отношением: отношение наследования, установленное между левой и верхней часть Right а также Top не может повлиять на эту связь. В частности, это означает, что Left::Top::foo() может быть переопределен в Left И в Bottom, но Right, который не имеет отношения к наследству с Left::Top, не может установить эту точку настройки.

Виртуальные базовые классы разные: виртуальное наследство - это общее отношение, которое можно настроить в производных классах:

struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { }; 
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { }; 

Здесь это только один субобект базового класса Top, только один int член.

Реализация:

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

Местоположение субобекта будет определено во время выполнения с VPTR и VTABLE (повторное использование существующего VPTR подразумевает меньше пространственных накладных расходов) или прямого внутреннего указателя на субобект (больше накладных расходов, меньше требуется).

Поскольку смещение виртуального базового класса определяется только для полного объекта и не может быть известно данным объявленным типом, Виртуальная база не может быть выделена при смещении ноль и никогда не является основной базой. Анкет Полученный класс никогда не будет повторно использовать VPTR виртуальной базы в качестве собственного VPTR.

С точки зрения возможного перевода:

struct vLeft__vtable { 
    int Top__offset; // relative vLeft-Top offset
    void (*foo__ptr) (vLeft *__this); 
    // additional virtual member function go here
};

// this is what a subobject of type vLeft looks like
struct vLeft__subobject { 
    vLeft__vtable const *__vptr;
    // data members go here
};

void vLeft__subobject__ctor (vLeft__subobject *__this) { 
    // initialise data members
}

// this is a complete object of type vLeft 
struct vLeft__complete {
    vLeft__subobject __sub;
    Top Top__base;
}; 

// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);

// virtual function implementation: call via base class
// layout is vLeft__complete 
void Top__in__vLeft__foo (Top *__this) {
    // inverse .Top__base member access 
    char *cp = reinterpret_cast<char*> (__this);
    cp -= offsetof (vLeft__complete,Top__base);
    vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
    vLeft__real__foo (__real);
}

void vLeft__foo (vLeft *__this) {
    vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}

// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = { 
    /*foo__ptr =*/ Top__in__vLeft__foo 
};

// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = { 
    /*Top__offset=*/ offsetof(vLeft__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

void vLeft__complete__ctor (vLeft__complete *__this) { 
    // construct virtual bases first
    Top__ctor (&__this->Top__base); 

    // construct non virtual bases: 
    // change dynamic type to vLeft
    // adjust both virtual base class vptr and current vptr
    __this->Top__base.__vptr = &Top__in__vLeft__real;
    __this->__vptr = &vLeft__real;

    vLeft__subobject__ctor (&__this->__sub);
}

Для объекта известного типа доступ к базовому классу проходит через vLeft__complete:

struct a_vLeft {
    vLeft m;
};

void f(a_vLeft &r) {
    Top &t = r.m; // upcast
    printf ("%p", &t);
}

переводится на:

struct a_vLeft {
    vLeft__complete m;
};

void f(a_vLeft &r) {
    Top &t = r.m.Top__base;
    printf ("%p", &t);
}

Здесь настоящий (динамический) тип r.m известен, как и относительное положение субобъекта известно во время компиляции. Но здесь:

void f(vLeft &r) {
    Top &t = r; // upcast
    printf ("%p", &t);
}

настоящий (динамический) тип r неизвестно, поэтому доступ через VPTR:

void f(vLeft &r) {
    int off = r.__vptr->Top__offset;
    char *p = reinterpret_cast<char*> (&r) + off;
    printf ("%p", p);
}

Эта функция может принять любой полученный класс с другой макетом:

// this is what a subobject of type vBottom looks like
struct vBottom__subobject { 
    vLeft__subobject vLeft__base; // primary base
    vRight__subobject vRight__base; 
    // data members go here
};

// this is a complete object of type vBottom 
struct vBottom__complete {
    vBottom__subobject __sub; 
    // virtual base classes follow:
    Top Top__base;
}; 

Обратите внимание, что vLeft базовый класс находится в фиксированном месте в vBottom__subobject, так vBottom__subobject.__ptr используется в качестве VPTR для всего vBottom.

Семантика:

Соотношение наследования разделяется всеми полученными классами; Это означает, что право на переопределение делится, поэтому vRight может переопределить vLeft::foo. Анкет Это создает обмен обязанностями: vLeft а также vRight должен согласиться с тем, как они настраивают Top:

struct Top { virtual void foo(); };
struct vLeft : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vRight : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vBottom : vLeft, vRight { };  // error

Здесь мы видим конфликт: vLeft а также vRight стремиться определить поведение единственной виртуальной функции Foo и vBottom Определение ошибочно из -за отсутствия общего переопределения.

struct vBottom : vLeft, vRight  { 
    override void foo(); // reconcile vLeft and vRight 
                         // with a common overrider
};

Реализация:

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

Строительство класса с не виртуальными базовыми классами с виртуальными базами сложнее: Во время строительства динамический тип является типом базового класса, но макет виртуальной базы по -прежнему остается макетом наиболее производного типа, который еще не построен, поэтому нам нужно больше VTables, чтобы описать это состояние:

// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = { 
    /*Top__offset=*/ offsetof(vBottom__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

Виртуальные функции - это функции vLeft (Во время строительства срок службы объекта Vbottom не начался), в то время как виртуальные базовые местоположения - это места vBottom (как определено в vBottom__complete Переведенный объект).

Семантика:

Во время инициализации очевидно, что мы должны быть осторожны, чтобы не использовать объект до его инициализации. Поскольку C ++ дает нам имя до того, как объект полностью инициализирован, это легко сделать:

int foo (int *p) { return *pi; }
int i = foo(&i); 

или с этим указателем в конструкторе:

struct silly { 
    int i;
    std::string s;
    static int foo (bad *p) { 
        p->s.empty(); // s is not even constructed!
        return p->i; // i is not set!
    }
    silly () : i(foo(this)) { }
};

Совершенно очевидно, что любое использование this В листе CTOR-INIT должен быть тщательно проверен. После инициализации всех членов, this может быть передано другим функциям и зарегистрирована в некотором наборе (до начала разрушения).

Что менее очевидно, так это то, что при строительстве класса с участием общих виртуальных оснований субобъекты перестают строить: во время строительства vBottom:

  • Сначала построены виртуальные основания: когда Top построен, он построен как обычный субъект (Top Даже не знает, что это виртуальная база)

  • затем базовые классы построены в левом образе вправо: vLeft субобект построен и становится функциональным как нормальный vLeft (но с vBottom макет), так что Top Base Class Subobject теперь имеет vLeft динамический тип;

  • а vRight начинается конструкция Subobject, и динамический тип базового класса изменяется на Vright; но vRight не происходит от vLeft, ничего не знает о vLeft, Итак vLeft База теперь сломана;

  • Когда тело Bottom начинается конструктор, типы всех субобъектов стабилизировались и vLeft снова функционально.

Я не уверен, как этот ответ может быть принят как полный ответ без упоминания о выравнивании или битах.

Позвольте мне дать немного фона выравнивания:

"Адрес памяти a, как говорят, выравнивается N-байт, когда A равен n-байтами (где n-мощность 2). В этом контексте байт-наименьшая единица доступа к памяти, то есть каждый адрес памяти указывает Другой байт. Уравнение n-байтового адреса будет иметь log2 (n) наименьших знакомых нулей при выраженном бинарном.

Альтернативная формулировка B-BIT Выравнивано назначено AB/8-байт-адресу (например, 64-битный выравнирован 8 байтов).

Говорят, что доступ к памяти выровнен, когда доступ к данным, доступ к N-байтам длиной, а адрес Datum выровнен N-байтовым. Когда доступ к памяти не выровнен, говорят, что он смещен. Обратите внимание, что по определению доступ к байтовой памяти всегда выровнен.

Указатель памяти, который относится к примитивным данным, длительным n байтами, считается, что это выровнен только, если его разрешают содержать адреса, которые выровнены N-байтовой, в противном случае он, как говорят, не поддается. Указатель памяти, который относится к агрегату данных (структура данных или массив), выровнен, если (и только если) каждая примитивная батам в совокупности выровнен.

Обратите внимание, что приведенные выше определения предполагают, что каждая примитивная батам является мощностью двух байтов длиной. Когда это не так (как при 80-битной плавающей точке на x86), контекст влияет на условия, в которых база считается выровненной или нет.

Структуры данных могут храниться в памяти в стеке со статическим размером, известным как ограниченный или на куче с динамическим размером, известным как неограниченное. » - от вики ...

Чтобы поддерживать выравнивание, компилятор вставляет биты накладки в скомпилированный код структуры/объекта класса. «Несмотря на то, что компилятор (или интерпретатор) обычно выделяет отдельные элементы данных по выравниваемым границам, структуры данных часто имеют члены с различными требованиями к выравниванию. Чтобы поддерживать надлежащее выравнивание, переводчик обычно вкладывает дополнительные неназванные элементы данных, чтобы каждый член был правильно выровнен. Кроме того Структура данных в целом может быть проделана окончательным неназванным членом. Это позволяет правильному выровнению каждого члена массива структур. .... ....

Заполнение вставлено только тогда, когда за элементом структуры следует элемент с более крупным требованием выравнивания или в конце структуры » - Вики

Чтобы получить больше информации о том, как это делает GCC, пожалуйста, посмотрите на

http://www.delorie.com/gnu/docs/gcc/gccint_111.html

и поиск текста «Основной адапт»

Теперь давайте придем к этой проблеме:

Используя пример класса, я создал эту программу для компилятора GCC, работающего на 64 -битном Ubuntu.

int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    A objA;
    C objC;
    cout<<__alignof__(objA.a)<<endl;
    cout<<sizeof(void*)<<endl;
    cout<<sizeof(int)<<endl;
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    cout<<sizeof(C)<<endl;
    cout<<__alignof__(objC.a)<<endl;
    cout<<__alignof__(A)<<endl;
    cout<<__alignof__(C)<<endl;
    return 0;
}

И результат этой программы - следующее:

4
8
4
16
16
32
4
8
8

Теперь позвольте мне объяснить это. Поскольку оба A & B имеют виртуальные функции, они будут создавать отдельные VTables, а VPTR будет добавляться в начале их объектов соответственно.

Следовательно, объект класса A будет иметь VPTR (указывая на Vtable a) и Int. Указатель будет длиной 8 байтов, а INT будет длиной 4 байта. Следовательно, перед сбором размера составляет 12 байт. Но компилятор добавит дополнительные 4 байта в конце int a в качестве битов. Следовательно, после компиляции размер объектов A будет 12+4 = 16.

Точно так же для объектов класса B.

Теперь объект C будет иметь два VPTR (по одному для каждого класса A & Class B) и 3 INT (A, B, C). Таким образом, размер должен был быть 8 (VPTR A) + 4 (int a) + 4 (байты прокладки) + 8 (vptr b) + 4 (int b) + 4 (int c) = 32 байта. Таким образом, общий размер C будет 32 байта.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top