Расположение объектов в случае виртуальных функций и множественного наследования - PullRequest
7 голосов
/ 24 августа 2009

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

  1. Какова точная схема памяти объекта класса С.
  2. Записи виртуальных таблиц для класса C.
  3. Размеры (возвращаемые sizeof) объектов классов 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;  
};   

Спасибо!

Ответы [ 4 ]

13 голосов
/ 24 августа 2009

Расположение памяти и размещение vtable зависят от вашего компилятора. Например, используя мой gcc, они выглядят так:

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

Обратите внимание, что sizeof (int) и пространство, необходимое для указателя vtable, также могут варьироваться от компилятора к компилятору и от платформы к платформе. Причина, по которой sizeof (C) == 20, а не 16, заключается в том, что gcc предоставляет ему 8 байтов для подобъекта A, 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 для получения этой информации.

4 голосов
/ 24 августа 2009

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

3 голосов
/ 27 июля 2015

Во-первых, у полиморфного класса есть хотя бы одна виртуальная функция, поэтому он имеет 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 (как это делал cfront) или даже на подмножество 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
};

Здесь «первая позиция» означает, что элемент должен быть первым (другие элементы могут быть переупорядочены): они расположены с нулевым смещением, поэтому мы можем 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 новое значение (в данном конкретном случае вызов конструктора базового класса не делает ничего полезного и может быть оптимизирован, но дело обстоит не так с нетривиальными конструкторами).

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

struct C : A, B {};

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

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

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

  • преобразование указателей на другие базовые классы (апкасты) потребует регулировка; и наоборот, для повышений нужны противоположные корректировки;

  • это означает, что при выполнении виртуального вызова с базовым классом указатель, 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 l и т. Д. (получено из Der), это также означает, что виртуальные функции A наследуются в Der: виртуальные функции в A могут быть переопределены в других производных классах.

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

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

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

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

В Bottom (Left::i и Right::i) есть только два Top::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 (обе имеют одинаковую сигнатуру и могут иметь общую переопределение).

Семантика не виртуальных базовых классов вытекает из того факта, что базовое, не виртуальное наследование является исключительным отношением: отношение наследования, установленное между Left и Top, не может быть изменено дальнейшим производным, поэтому существует тот же факт между 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
};

Реализация:

Конструирование класса с не виртуальными базовыми классами с не виртуальными базовыми классами включает вызов конструкторов базового класса в том же порядке, что и для переменных-членов, меняя динамический тип каждый раз, когда мы вводим ctor. Во время построения подобъекты базового класса действительно действуют так, как если бы они были законченными объектами (это даже верно для невозможных завершенных подобъектов базового класса: они являются объектами с неопределенными (чистыми) виртуальными функциями). Виртуальные функции и 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 переведенном возражении). 1207 *

Семантика:

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

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

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

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-list должно быть тщательно проверено. После инициализации всех членов this может быть передано другим функциям и зарегистрировано в некотором наборе (пока не начнется уничтожение).

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

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

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

  • начинается создание подобъекта vRight, и динамический тип базового класса меняется на vRight; но vRight не является производным от vLeft, ничего не знает о vLeft, поэтому база vLeft теперь сломана;

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

0 голосов
/ 20 июля 2015

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

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

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

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

Доступ к памяти считается выровненным, когда доступ к данным имеет длину n байтов, а адрес базы данных выровнен по n байтов. Когда доступ к памяти не выровнен, он считается смещенным. Обратите внимание, что по определению доступ к байтовой памяти всегда выравнивается.

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

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

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

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

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

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

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

и поиск текста "basic-align"

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

Используя пример класса, я создал эту программу для компилятора 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 имеют виртуальные функции, они будут создавать отдельные VTABLE, и VPTR будет добавляться в начале их объектов соответственно.

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

Аналогично для объектов класса B.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...