C ++ выравнивание данных / порядок членов и наследование - PullRequest
26 голосов
/ 05 января 2010

Как члены данных выравниваются / упорядочиваются, если используется наследование / множественное наследование? Этот компилятор специфичен?

Есть ли способ указать в производном классе, как элементы (включая членов из базового класса) должны быть упорядочены / выровнены?

Спасибо!

Ответы [ 8 ]

61 голосов
/ 05 января 2010

На самом деле вы задаете здесь много разных вопросов, поэтому я постараюсь ответить на каждый из них по очереди.

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

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

Например:

struct some_object
{
    char c;
    double d;
    int i;
};

Эта структура будет 24 байта. Поскольку класс содержит double, он будет выровнен на 8 байтов, то есть символ будет дополнен на 7 байтов, а int будет дополнен на 4, чтобы гарантировать, что в массиве some_object все элементы будут выровнены на 8 байтов. Вообще говоря, это зависит от компилятора, хотя вы обнаружите, что для данной архитектуры процессора большинство компиляторов выравнивают данные одинаково.

Второе, что вы упоминаете, это производные ученики. Упорядочение и выравнивание производных классов является своего рода болью. Классы индивидуально следуют правилам, которые я описал выше для структур, но когда вы начинаете говорить о наследовании, вы попадаете в грязный газон. Даны следующие классы:

class base
{
    int i;
};

class derived : public base // same for private inheritance
{
    int k;
};

class derived2 : public derived
{
    int l;
};

class derived3 : public derived, public derived2
{
    int m;
};

class derived4 : public virtual base
{
    int n;
};

class derived5 : public virtual base
{
    int o;
};

class derived6 : public derived4, public derived5
{
    int p;
};

Структура памяти для базы будет:

int i // base

Структура памяти для производной будет:

int i // base
int k // derived

Структура памяти для производного 2 будет:

int i // base
int k // derived
int l // derived2

Структура памяти для производного 3 будет:

int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3

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

Чтобы обойти это, у нас есть виртуальное наследование.

Структура памяти для производного 4 будет:

base* base_ptr // ptr to base object
int n // derived4
int i // base

Структура памяти для производного 5 будет:

base* base_ptr // ptr to base object
int o // derived5
int i // base

Структура памяти для производного 6 будет:

base* base_ptr // ptr to base object
int n // derived4
int o // derived5
int i // base

Вы заметите, что производные 4, 5 и 6 имеют указатель на базовый объект. Это необходимо, так что при вызове любой из функций базы у нее есть объект для передачи этим функциям. Эта структура зависит от компилятора, потому что она не указана в спецификации языка, но почти все компиляторы реализуют ее одинаково.

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

class vbase
{
    virtual void foo() {};
};

class vbase2
{
    virtual void bar() {};
};

class vderived : public vbase
{
    virtual void bar() {};
    virtual void bar2() {};
};

class vderived2 : public vbase, public vbase2
{
};

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

Структура памяти для vbase будет:

void* vfptr // vbase

Структура памяти для vbase2 будет:

void* vfptr // vbase2

Структура памяти для vderived будет:

void* vfptr // vderived

Структура памяти для vderived2 будет:

void* vfptr // vbase
void* vfptr // vbase2

Многие люди не понимают, как работают vftables. Первое, что нужно понять, это то, что классы хранят только указатели на vftables, а не на целые vftables.

Это означает, что независимо от того, сколько виртуальных функций имеет класс, у него будет только одна vftable, если только он не наследует vftable откуда-то еще через множественное наследование. Практически все компиляторы помещают указатель vftable перед остальными членами класса. Это означает, что между указателем vftable и членами класса может быть некоторое отступление.

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

Последнее, что вы спросили, можете ли вы контролировать порядок. Вы всегда контролируете порядок. Компилятор всегда упорядочивает вещи в том порядке, в котором вы их записываете. Надеюсь, это многословное объяснение затронет все, что вам нужно знать.

3 голосов
/ 05 января 2010

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

Если вы делаете что-то, что зависит от порядка и упаковки, попробуйте сохранить структуру POD внутри вашего класса и использовать это.

1 голос
/ 27 июня 2013

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

Я проверил это с помощью msdev и g ++, и они оба переставили классы. Досадно, что у них, похоже, разные правила, как они это делают. Если у вас есть 3 или более базовых классов, а у первого нет виртуальных функций, эти компиляторы будут предлагать разные макеты.

Чтобы быть в безопасности, выберите два и избегайте другого.

  1. Не полагайтесь на порядок базовых классов при использовании множественного наследования.

  2. При использовании множественного наследования поместите все базовые классы с виртуальными функциями перед любыми базовыми классами без виртуальных функций.

  3. Используйте 2 или меньше базовых классов (поскольку в этом случае компиляторы переставляют одинаково)

1 голос
/ 05 января 2010

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

так

struct foo
{
    char a;
    int b;
    char c;
}

Обычно занимает 32 байта для 32-битного компьютера

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

При множественном наследовании существует смещение между адресом класса и адресом второго базового класса. >static_cast и dynamic_cast рассчитают смещение. reinterpret_cast нет. Приведения в стиле C выполняют статическое приведение, если это возможно, в противном случае - переосмысление.

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

1 голос
/ 05 января 2010

Как только ваш класс не POD (простые старые данные), все ставки отменяются. Вероятно, существуют специфичные для компилятора директивы, которые можно использовать для упаковки / выравнивания данных.

1 голос
/ 05 января 2010

Это зависит от компилятора.

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

0 голосов
/ 13 сентября 2015

Я могу ответить на один из вопросов.

Как выровнять / упорядочить элементы данных, если используется наследование / множественное наследование?

Я создал инструмент для визуализации макета памяти классов, стековых фреймов функций и другой информации ABI (Linux, GCC). Вы можете посмотреть на результат для mysqlpp :: Connection class (наследует OptionalExceptions) из библиотеки MySQL ++ здесь .

enter image description here

0 голосов
/ 05 января 2010

Все известные мне компиляторы помещают объект базового класса перед членами данных в производном объекте класса. Элементы данных в порядке, указанном в объявлении класса. Там могут быть пробелы из-за выравнивания. Я не говорю, что так должно быть.

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