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

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

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] ?), т. е. нет заполнения before первый член.

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

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

person lisyarus    schedule 12.04.2019    source источник
comment
Первая из двух оставшихся гарантий гарантируется C++ 2017 (черновик n4659) 11.3.4, «Массивы» [dcl.array]: «Объект типа массива содержит непрерывно выделенный непустой набор N подобъектов типа T. ” Издание 1998 г. имеет идентичный текст, за исключением слов «подобъекты» в 8.3.4, разделенных дефисом.   -  person Eric Postpischil    schedule 12.04.2019
comment
@EricPostpischil Спасибо за разъяснения! Что именно означает смежное размещение в этом контексте?   -  person lisyarus    schedule 12.04.2019
comment
@lisyarus: Это «простой английский» или, по крайней мере, английский язык, используемый практиками в этой области — он формально не определен в стандарте. Я совершенно уверен, что это означает, что байты элементов в массиве располагаются в памяти один за другим без заполнения между элементами.   -  person Eric Postpischil    schedule 12.04.2019
comment
В C вторая из оставшихся гарантий не гарантируется, и есть некоторые причины, по которым «сложная» реализация C может дополнять структуру, содержащую один массив. Например, мы можем представить, что реализация дополнит struct { char x[2]; } четырьмя байтами, если ее целевое оборудование имеет сильный уклон в сторону четырехбайтовой адресации памяти, и реализация решила сделать все структуры выровненными по крайней мере по четырем байтам, чтобы удовлетворить Требование стандарта C к одному представлению для всех указателей структур. Я ожидаю, что C++ похож, но не могу с уверенностью говорить о нем…   -  person Eric Postpischil    schedule 12.04.2019
comment
… и обратите внимание, что это что-то вроде «теоретической» возможности. Вероятнее всего, struct { float data[16]; } не будет иметь завершающего дополнения ни в одной обычной реализации C или C++ — для этого нет причин ни на одной нормальной целевой платформе. Но в отсутствие явной спецификации в стандарте C++ единственный способ гарантировать это для проекта — потребовать, чтобы любая реализация C++, используемая для его компиляции, удовлетворяла этому свойству. Это можно проверить с помощью утверждения.   -  person Eric Postpischil    schedule 12.04.2019
comment
1-й очевиден, но это не сильно помогает, если 2-й не гарантирован, и в любом случае нелегко найти какую-либо информацию. Самое близкое, что я нашел, находится в class.mem. Но это говорит о промежуточных элементах данных и о начале, а не о конце. Тем не менее, утверждения sizeof должны проходить для массива. sizeof должен включать любые отступы в конце.   -  person luk32    schedule 12.04.2019
comment
@EricPostpischil Спасибо. Как я уже сказал, все платформы и компиляторы, с которыми я обычно работаю, имеют ожидаемую структуру памяти для этой примерной структуры, поэтому мой вопрос также носит скорее теоретический характер.   -  person lisyarus    schedule 12.04.2019
comment
Тривиальная структура памяти массива C подразумевается способом выполнения арифметических операций с указателями.   -  person curiousguy    schedule 14.04.2019


Ответы (2)


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

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

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

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

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

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

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

person eerorika    schedule 12.04.2019
comment
Просто чтобы быть абсолютно ясным. Является ли ваш последний абзац прямым контрпримером против 2-го вопроса без ответа? Я спрашиваю с точки зрения составного типа, поэтому, например, структура S {char a,b,c;};, если она дополнена до 4*sizeof(char), может иметь отступы в конце. И в этом отношении мы не можем указать относительный адрес любого члена, кроме a, я думаю, что они могут быть переупорядочены и дополнены по усмотрению компилятора. Ага? - person luk32; 12.04.2019
comment
@luk32 Luk32 Не может быть необходимости в заполнении между элементами char, поскольку они имеют выравнивание 1. Любой разумный ABI поместит дополнение (если оно есть) S в конец. Но действительно, я не знаю явной гарантии этого в стандарте C++. - person eerorika; 12.04.2019
comment
Не могли бы вы подробнее рассказать об этом использовании std::launder? - person lisyarus; 12.04.2019
comment
@eerorika Прошу прощения, но я изо всех сил пытаюсь понять причину, по которой здесь нужен std::launder, основываясь на статье cppreference. - person lisyarus; 12.04.2019
comment
@lisyarus Указатель на A можно переинтерпретировать, приведя к указателю на float[16], потому что тип первого члена (data) стандартного класса макета A равен float[16] eel.is/c++draft/basic.compound#4. Если бы указатель на float[16] был взаимопреобразуем с указателем на float (data[0]), то указатель на A был бы транзитивно конвертируем в float... - person eerorika; 12.04.2019
comment
... Но массив не является взаимопреобразуемым указателем с первым элементом, поэтому предпосылка не выполняется. Насколько я понимаю, std::launder должно работать преобразование. На странице cppreference есть пример, выполняющий преобразование в другом направлении (от первого элемента к типу массива) с использованием отмывания с комментарием OK. - person eerorika; 12.04.2019
comment
@eerorika Спасибо за разъяснения. Думаю, мне придется немного покопаться в std::launder. - person lisyarus; 13.04.2019
comment
@lisyarus В C и C++ указатель не указывает на место в памяти, он указывает на назначенный объект: указатели — это типы высокого уровня, а не адреса низкого уровня, как в ассемблере. Указание на объект — это не то же самое, что указание на другой объект по тому же адресу. Я задавал много вопросов, связанных с указателями (почти все они были очень плохо восприняты), называя это семантическим значением указателя, а не числовым значением. аверс если скрестить комп. границы и функция вызова, скомпилированная diff comp. имеет значение только числовое значение (состояние) объекта указателя (описано ABI). - person curiousguy; 14.04.2019
comment
(...) C++ притворяется, что указатели являются тривиальными типами. Нет вежливого способа объяснить это. Это ложь. Указатели не могут быть тривиального типа, поскольку семантическое значение тривиального типа является функцией его битового шаблона, точки. Если два объекта тривиального типа, ни один из которых не является неинициализированным, имеют одинаковую битовую комбинацию и имеют одинаковое семантическое значение, период; это означает, что любая операция, допустимая для одного, имеет такую ​​же действительность для другого. Если один тривиальный тип ptr может быть разыменован, то любой другой объект-указатель того же типа и битового шаблона может быть разыменован, и вы получите то же самое. - person curiousguy; 14.04.2019
comment
(...) Таким образом, если не привязанный указатель к объекту массива имеет то же значение, что и указатель к другому объекту, вы должны иметь возможность разыменовать его. На практике это не сработает, компиляторы этого не позволяют. int a[1], b[1]; a[1] = 2; не является законным способом доступа к b[0], даже если его адрес совпадает с адресом после конца a+1 ptr.< /b> Доказательство того, что ptr имеют числовое значение и семантическое значение, и что способ их получения (их происхождение) определяет их семантическое значение. Это не ясная концепция в сознании большинства людей, и я получил ужасную критику, просто подняв Q здесь. - person curiousguy; 14.04.2019

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

Таким образом, стандарт гарантирует, что

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));
person Yashas    schedule 12.04.2019