Как определяется размер класса С++?

Резюме. Как компилятор статически определяет размер класса С++ во время компиляции?

Подробнее:

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

Например, следующий код объявляет 4 класса. Первые 2 равны 16 байт. Но 3 - 48 байтов, хотя в нем есть те же самые элементы данных, что и первые 2. В то время как четвертый класс имеет те же элементы данных, что и третий, только в другом порядке, но это 32 байта.

#include <xmmintrin.h>
#include <stdio.h>

class TestClass1 {
  __m128i vect;
};

class TestClass2 {
  char buf[8];
  char buf2[8];
};

class TestClass3 {
  char buf[8];
  __m128i vect;
  char buf2[8];
};

class TestClass4 {
  char buf[8];
  char buf2[8];
  __m128i vect;
};


TestClass1 *ptr1;
TestClass2 *ptr2;
TestClass3 *ptr3;
TestClass4 *ptr4;
int main() {
  ptr1 = new TestClass1();
  ptr2 = new TestClass2();
  ptr3 = new TestClass3();
  ptr4 = new TestClass4();
  printf("sizeof TestClass1 is: %lu\t TestClass2 is: %lu\t TestClass3 is: %lu\t TestClass4 is: %lu\n", sizeof(*ptr1), sizeof(*ptr2), sizeof(*ptr3), sizeof(*ptr4));
  return 0;
}

Я знаю, что ответ имеет отношение к выравниванию членов данных класса. Но я пытаюсь точно понять, что это за правила и как они применяются во время шагов компиляции, потому что у меня есть класс, у которого есть элемент данных __m128i, но элемент данных не выровнен по 16 байт, и это приводит к segfault когда компилятор генерирует код с помощью movaps для доступа к данным.

Ответ 1

Для POD (простые старые данные) правило обычно:

  • Каждый член в структуре имеет некоторые размеры s и некоторые требования к выравниванию a.
  • Компилятор начинается с размера S, установленного на ноль, и требования к выравниванию A, установленного в один (байт).
  • Компилятор обрабатывает каждый элемент в структуре по порядку:
    • Рассмотрим требование выравнивания элементов a. Если S в настоящее время не кратно a, тогда добавьте достаточно байтов S, чтобы он был кратным a. Это определяет, куда будет идти член; он будет идти со смещением S от начала структуры (для текущего значения S).
    • Задайте A наименьшим общим кратным A и a.
    • Добавьте s в S, чтобы выделить место для члена.
  • Когда вышеописанный процесс выполняется для каждого члена, рассмотрим требование выравнивания структуры A. Если S в настоящее время не кратно A, тогда добавьте достаточно для S, чтобы он был кратным A.

Размер структуры - это значение S, когда это сделано.

Дополнительно:

  • Если какой-либо элемент является массивом, его размер представляет собой число элементов, умноженное на размер каждого элемента, а его требование к выравниванию - требование выравнивания элемента.
  • Если какой-либо элемент является структурой, его размер и требование выравнивания вычисляются, как указано выше.
  • Если какой-либо член является объединением:
    • Установите S в размер самого большого члена.
    • Задайте A наименьшим общим кратным выравниваний всех членов.
    • Если S не кратно A, добавьте достаточно для S, чтобы сделать его кратным A.

Рассмотрим ваш TestClass3:

  • S начинается с 0 и начинается с 1.
  • char buf[8] требуется 8 байт и выравнивание 1, поэтому S увеличивается на 8 до 8, а A остается 1.
  • __m128i vect требуется 16 байт и выравнивание 16. Сначала S необходимо увеличить до 16, чтобы дать правильное выравнивание. Тогда A должно быть увеличено до 16. Тогда S должно быть увеличено на 16, чтобы сделать пробел для vect, поэтому S теперь 32.
  • char buf2[8] требуется 8 байт и выравнивание 1, поэтому S увеличивается на 8-24, а A остается 16.
  • В конце S равно 24, что не кратно A (16), поэтому S должно быть увеличено на 8-32.

Таким образом, размер TestClass3 составляет 32 байта.

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

Ответ 2

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

Однако поведение, которое вы наблюдали, довольно типично. Компилятор пытается выровнять элементы так, чтобы каждый из них начинал с кратного их размера. В случае TestClass3 один из членов имеет тип __m128i и sizeof(__m128i) == 16. Поэтому он попытается выровнять этот член для начала с байтом, который будет кратным 16. Первый член имеет тип char[8], поэтому занимает 8 байтов. Если компилятор должен был разместить объект _m128i непосредственно после этого первого элемента, он начинался бы в позиции 8, которая не кратно 16:

0               8               16              24              32              48
┌───────────────┬───────────────────────────────┬───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │            __m128i            │    char[8]    │           
└───────────────┴───────────────────────────────┴───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

Поэтому вместо этого он предпочитает это делать:

0               8               16              24              32              48
┌───────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┬───────────────────────────────┬───────────────┐┄┄┄
│    char[8]    │               │           __m128i             │    char[8]    │
└───────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┴───────────────────────────────┴───────────────┘┄┄┄

Это дает размер 48 байт.

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

0               8               16              24              32              48
┌───────────────┬───────────────┬───────────────────────────────┬┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
│    char[8]    │    char[8]    │           __m128i             │        
└───────────────┴───────────────┴───────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄

Теперь все правильно выровнено - массивы находятся в смещениях, которые кратно 1 (размер их элементов), а объект __m128i имеет смещение, кратное 16, а общий размер - 32 байта.

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

Нестационарные члены данных (non-union) класса с одним и тем же контролем доступа (раздел 11) распределяются таким образом, что более поздние члены имеют более высокие адреса в объекте класса.

Ответ 3

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

Для GCC это Itanium ABI.

(К сожалению, он больше не доступен для публики, хотя я нашел зеркало.)

Ответ 4

если вы хотите обеспечить привязку, вы должны использовать "pragma pack (1)" в h файле посмотрите этот пост: http://tedlogan.com/techblog2.html