Гарантированное расположение памяти для стандартной структуры размещения с одним элементом массива примитивного типа

Рассмотрим следующую простую структуру:

struct A
{
    float data[16];
};

Мой вопрос:

Предполагая, что платформа с float представляет собой 32-битное число с плавающей точкой IEEE754 (если это вообще имеет значение), гарантирует ли стандарт C++ ожидаемый макет памяти для struct A? Если нет, что это гарантирует и/или как обеспечить соблюдение гарантий?

Под ожидаемой компоновкой памяти я подразумеваю, что структура занимает 16*4=64 байта в памяти, каждый из 4 последовательных байтов занят одним float из массива data. Другими словами, ожидаемое расположение памяти означает следующие прохождения теста:

static_assert(sizeof(A) == 16 * sizeof(float));
static_assert(offsetof(A, data[0]) == 0 * sizeof(float));
static_assert(offsetof(A, data[1]) == 1 * sizeof(float));
...
static_assert(offsetof(A, data[15]) == 15 * sizeof(float));

(offsetof здесь допустим, поскольку A - стандартное расположение, см. ниже)

В случае, если это вас беспокоит, тест фактически проходит на wandbox с gcc 9 HEAD. Я никогда не встречал комбинацию платформы и компилятора, которая бы свидетельствовала о том, что этот тест может провалиться, и я хотел бы узнать о них в случае их существования.

Зачем кому-то все равно:

  • Подобные SSE оптимизации требуют определенной компоновки памяти (и выравнивания, которое я игнорирую в этом вопросе, поскольку с ним можно справиться, используя стандартный alignas).
  • Сериализация такой структуры будет просто сводиться к красивому и переносимому write_bytes(&x, sizeof(A)).
  • Некоторые API (например, OpenGL, в частности, скажем, glUniformMatrix4fv) ожидают такого точного расположения памяти. Конечно, можно просто передать указатель на массив data чтобы передать один объект этого типа, но для последовательности из них (скажем, для загрузки атрибутов вершин матричного типа) определенная схема памяти все еще необходима.

Что на самом деле гарантировано:

Это те вещи, которые, насколько мне известно, можно ожидать от struct A:

  • Это стандартная раскладка
  • Как следствие стандартного размещения, указатель на A может быть reinterpret_cast для указателя на его первый элемент данных (который, по-видимому, data[0]?), То есть нет заполнения перед первым элементом.

Две оставшиеся гарантии, которые не предоставлены (насколько мне известно) стандартом:

  • Между элементами массива примитивного типа нет заполнения (я уверен, что это неверно, но мне не удалось найти подтверждающую ссылку),
  • Заполнение после массива data внутри struct A.

Ответы

Ответ 1

Одна вещь, которая не гарантируется в компоновке, это порядковый номер, то есть порядок байтов в многобайтовом объекте. write_bytes(&x, sizeof(A)) не является переносимой сериализацией в системах с различным порядком байтов.

A может быть reinterpret_cast для указателя на его первый элемент данных (который, предположительно, data[0]?)

Исправление: Первый элемент данных - это data, которые вы можете интерпретировать с помощью приведения. И что очень важно, массив не является взаимозаменяемым по указателю с его первым элементом, поэтому вы не можете интерпретировать приведение между ними. Однако адрес гарантированно будет таким же, поэтому, как я понимаю, после std::launder должно быть хорошо истолковано как data[0].

Между элементами массива примитивного типа нет заполнения

Массивы гарантированно будут смежными. sizeof объекта указывается в терминах заполнения, необходимого для размещения элементов в массиве. sizeof(T[10]) имеет в точности размер sizeof(T * 10). Если между не дополняющими битами смежных элементов есть заполнение, то это заполнение находится в конце самого элемента.

Примитивный тип не гарантированно не имеет отступов вообще. Например, x86 повышенной точностью long double 80 бит, проложенные до 128 бит.

char, signed char и unsigned char гарантированно не будут иметь битов заполнения. Стандарт C (которому в данном случае C++ делегирует спецификацию) гарантирует, что псевдонимы фиксированной ширины intN_t и uintN_t не имеют битов заполнения. В системах, где это невозможно, эти типы фиксированной ширины не предусмотрены.

Ответ 2

Если объект класса стандартной компоновки имеет какие-либо нестатические элементы данных, его адрес совпадает с адресом его первого нестатического члена данных. В противном случае его адрес совпадает с адресом его первого подобъекта базового класса (если есть). [Примечание: Следовательно, в объекте структуры стандартной компоновки может быть безымянный отступ, но не в его начале, что необходимо для достижения соответствующего выравнивания. - конец примечания]

Следовательно, стандарт гарантирует, что

static_assert(offsetof(A, data[0]) == 0 * sizeof(float));

Объект типа массив содержит непрерывно размещенный непустой набор из N подобъектов типа T.

Следовательно, верно следующее

static_assert(offsetof(A, data[0]) == 0 * sizeof(float));
static_assert(offsetof(A, data[1]) == 1 * sizeof(float));
...
static_assert(offsetof(A, data[15]) == 15 * sizeof(float));

Ответ 3

Похоже, что с С++ 11 вы можете управлять выравниванием структуры с alignas спецификатора alignas

struct alignas(sizeof(float) * 16) A
{
    float data[16];
};