выравнивание данных c ++ / порядок членов и наследование

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

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


person genesys    schedule 05.01.2010    source источник
comment
связанные: http://stackoverflow.com/questions/2006216/why-is-data-structure-alignment-important-for-performance   -  person jldupont    schedule 05.01.2010
comment
Помните, что размер структуры (или класса) не может быть равен сумме размеров ее членов, прежде всего потому, что компиляторам разрешено добавлять отступы между членами. Более надежный метод - прочитать упакованные данные в буфер unsigned char, а затем загрузить элементы из этого буфера. Точно так же записывать члены в буфер, а вывод - в буфер. Это предотвратит проблемы с выравниванием или упаковкой, которые могут нанести ущерб вашей программе.   -  person Thomas Matthews    schedule 05.01.2010
comment
я не беспокоюсь о заполнении - заполнение в порядке - я просто хочу иметь возможность предсказать формат необработанных данных простой структуры, которая является потомком нескольких других простых структур   -  person Mat    schedule 05.01.2010


Ответы (9)


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

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

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

Например:

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

Эта структура будет иметь размер 24 байта. Поскольку класс содержит double, он будет выровнен на 8 байт, то есть char будет дополнен 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 будет следующей:

void* base_ptr // implementation defined ptr that allows to find base
int n // derived4
int i // base

Схема памяти для производного5 будет следующей:

void* base_ptr // implementation defined ptr that allows to find base
int o // derived5
int i // base

Схема памяти для производного6 будет следующей:

void* base_ptr // implementation defined ptr that allows to find base
int n // derived4
void* base_ptr2 // implementation defined ptr that allows to find base
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

Схема памяти для vderoduction будет следующей:

void* vfptr // vderived

Схема памяти для vdehibited2 будет следующей:

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

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

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

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

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

person Beanz    schedule 05.01.2010
comment
да - это очень очень хорошо! большое спасибо! еще один вопрос - действуют ли эти правила, если определены конструкторы и деструкторы, отличные от заданных по умолчанию? - person genesys; 05.01.2010
comment
Абсолютно. Конструкторы и деструкторы не влияют на структуру класса, если деструктор не является виртуальным, и в этом случае должен присутствовать vfptr. Кроме того, я не стал вдаваться в подробности, потому что это немного выходит за рамки вопроса, но будьте осторожны с порядком инициализации. Инициализация всегда происходит от младшего адреса памяти к старшему, за исключением случая виртуального наследования, когда виртуально унаследованные объекты конструируются первыми. Аналогичным образом деструкторы вызываются от старшего адреса к младшему, за исключением случая виртуального наследования, когда деструктор виртуального унаследованного класса вызывается последним. - person Beanz; 05.01.2010
comment
Я только что прочитал следующее (в другом контексте): стандарт языка говорит, что побайтовые копии гарантированно работают только для POD. std :: pair ‹T, U› не является агрегатом классов, поскольку он имеет определяемый пользователем конструктор, а это означает, что он также не является POD. это просто плохое объяснение того, почему нельзя предсказать структуру памяти std :: pair ‹T, U› или что-то меняется, когда присутствует определяемый пользователем конструктор? - person Mat; 06.01.2010
comment
Это сложный вопрос, но я постараюсь ответить на него кратко. После указания конструктора, объявленного пользователем (его даже не нужно определять), больше нет гарантии, что объект имеет конструктор по умолчанию, что означает, что он не является POD. Это не означает, что его нельзя скопировать с помощью memcpy, это просто означает, что язык не гарантирует, что memcpy сделает истинную копию объекта. Причина этого (я считаю) в том, что гарантировать побайтовое копирование сложных или полиморфных объектов было бы головной болью и, вероятно, привело бы к более медленному выполнению кода. - person Beanz; 06.01.2010
comment
применяются ли правила также, если используется шаблон? предположим, что база является шаблоном и хранит значение типа T вместо int (предположим, что в качестве параметра шаблона используются только типы POD) - будет ли int просто заменяться типом типа шаблона или тогда происходит какая-то дополнительная магия? - person genesys; 06.01.2010
comment
Использование шаблонов ни в коем случае не должно изменять такое поведение. Классы шаблонов создаются во время компиляции, и для каждой комбинации используемых параметров шаблона создается уникальный класс. - person Beanz; 06.01.2010
comment
Есть одно исключение в отношении заказа. Порядок битовых полей, по-видимому, определяется реализацией (и обычно зависит от порядка байтов) - person Swift - Friday Pie; 28.08.2019
comment
Макет памяти для derived6 не включает локальное поле p - по-видимому, это непреднамеренно, и оно должно появиться сразу после базового ptr, но может ли кто-нибудь более квалифицированный, чем я, подтвердить / опровергнуть это и обновить сообщение, чтобы исправить / уточнить? - person DaveRandom; 09.12.2019

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

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

person JoeG    schedule 05.01.2010
comment
рассматриваемые структуры данных ЯВЛЯЮТСЯ структурами POD (по крайней мере, если множественное наследование от других структур POD по-прежнему приводит к структуре POD). будут ли тогда члены просто упорядочены чем-то вроде 'basePODStruct1_members'-padding-'basePODStruct2_members'-padding -...' производногоPODStruct_members'-padding? - person genesys; 05.01.2010
comment
@genesys: если есть какое-либо наследование (или виртуальные функции, или конструкторы, или деструкторы), то структура не является POD. - person Mike Seymour; 05.01.2010
comment
Заполнение между членами зависит от компилятора. Порядок членов определяется спецификацией языка. - person Thomas Matthews; 05.01.2010

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

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

person Goz    schedule 05.01.2010
comment
Нет, только отступы между членами зависит от компилятора. Порядок членов определяется спецификациями языка. - person Thomas Matthews; 05.01.2010
comment
Размещение виртуального стола не определено. Это просто должно быть там. В прошлом существовала разница между тем, как VC и GCC размещали виртуальные таблицы в случаях множественного наследования ... - person Goz; 06.01.2010

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

person James    schedule 05.01.2010
comment
Предпочтительно не использовать директивы компилятора для упаковки структуры, а использовать методы для чтения и записи членов в упакованный буфер. Это дает более надежную программу, особенно при изменении поставщика или версии компилятора. - person Thomas Matthews; 05.01.2010
comment
Да, но у вас есть больше кода, который нужно поддерживать, и ввести область для ошибок, если структура изменится. Вероятно, ваши структуры меняются чаще, чем ваш компилятор;) Конечно, когда вы начинаете, нет конца стратегиям сериализации / десериализации, разработанным для решения обеих проблем. - person James; 05.01.2010

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

so

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

Обычно занимает более 6 байт для 32-битной машины

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

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

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

person doron    schedule 05.01.2010
comment
структуры обрабатываются так же, как классы для выравнивания и упаковки. Важно то, является ли структура / класс POD или нет. - person JoeG; 05.01.2010
comment
Смещение может быть даже при одиночном наследовании. Когда класс с виртуальными функциями является производным от типа POD, популярные компиляторы упорядочивают память как vtableptr + POD + derived - person Drew Dormann; 05.01.2010

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

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

На всякий случай выберите два и избегайте другого.

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

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

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

person mccoyn    schedule 27.06.2013

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

person user231967    schedule 05.01.2010
comment
Порядок членов определяется спецификацией языка, включая наследование. Отступы между членами и классами во время наследования определяются реализацией. - person Thomas Matthews; 05.01.2010
comment
просто из любопытства - как получается, что отступы не указаны языком? - person Mat; 06.01.2010

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

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

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

введите описание изображения здесь

person linuxbuild    schedule 12.09.2015

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

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

Выравнивание класса / структуры равно наибольшему выравниванию его членов (теоретически зависит от реализации).

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

Завершающие отступы добавляются, чтобы размер объекта был кратен его выравниванию.

Сложный пример,

struct base1 {
  char m_tag;
  int m_base1;
  base1() : m_tag(0x11), m_base1(0x1b1b1b1b) { }
};

struct derived1 : public base1 {
  char m_tag;
  alignas(16) int m_derived1;
  derived1() : m_tag(0x21), m_derived1(0x1d1d1d1d) { }
};

struct derived2 : virtual public derived1 {
  char m_tag;
  int m_derived2_a;
  int m_derived2_b;
  derived2() : m_tag(0x31), m_derived2_a(0x2d2daa2d), m_derived2_b(0x2d2dbb2d) { }
};

struct derived3 : virtual public derived1 {
  char m_tag;
  int m_derived3;
  virtual ~derived3() { }
  derived3() : m_tag(0x41), m_derived3(0x3d3d3d3d) { }
};

struct base2 {
  char m_tag;
  int m_base2;
  virtual ~base2() { }
  base2() : m_tag(0x51), m_base2(0x2b2b2b2b) { }
};

struct derived4 : public derived2, public base2, public derived3 {
  char m_tag;    
  int m_derived4;
  derived4() : m_tag(0x61), m_derived4(0x4d4d4d4d) { }
};

Имеет следующую схему памяти:

 derived4 = derived2 -> ....P....O....I....N....T....E....R....
  subobject derived2 -> 0x31 padd padd padd 0x2d 0xaa 0x2d 0x2d 
                        0x2d 0xbb 0x2d 0x2d padd padd padd padd 
virual table = base2 -> ....P....O....I....N....T....E....R....
     subobject base2 -> 0x51 padd padd padd 0x2b 0x2b 0x2b 0x2b 
            derived3 -> ....P....O....I....N....T....E....R....
  subobject derived3 -> 0x41 padd padd padd 0x3d 0x3d 0x3d 0x3d 
  subobject derived4 -> 0x61 padd padd padd 0x4d 0x4d 0x4d 0x4d 
    derived1 = base1 -> 0x11 padd padd padd 0x1b 0x1b 0x1b 0x1b 
  subobject derived1 -> 0x21 padd padd padd padd padd padd padd 
                        0x1d 0x1d 0x1d 0x1d padd padd padd padd 
                        padd padd padd padd padd padd padd padd

Обратите внимание, что после приведения производного4 объекта к производному2 или производному3 новый объект начинается с указателя на виртуальный базовый класс, который находится где-то внизу на изображении производного4, как и реальный производный2 или производный3 объект.

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

Порядок элементов следующий: сначала (указатель виртуального базового класса и) элементы производного2, затем (указатель виртуальной таблицы и) элементы базы, (указатель виртуального базового класса и) элементы производного3 и, наконец, элементы ( подобъект) производный4 - все, за которым следует виртуальный базовый класс производный1.

Также обратите внимание, что, хотя реальный объект «производный3» должен быть выровнен по 16 байтам, потому что он «содержит» (в конце) виртуальный базовый класс производный1, который выровнен по 16, потому что у него есть член, выровненный по 16; но «производный3», который используется здесь в множественном наследовании, НЕ выровнен по 16 байтам. Это нормально, потому что производный3 без виртуального базового класса имеет макс. выравнивание всего 8 (указатель виртуального базового класса; это на 64-битной машине).

person Carlo Wood    schedule 16.01.2020