Законно ли псевдоним структура и массив?
Арифметика указателей между последовательными членами одного типа в структуре была обычной практикой, в то время как арифметика указателей действительна только внутри массива. В C++ это было бы явно неопределенным поведением, потому что массив может быть создан только объявлением или новым выражением. Но язык C определяет массив как непрерывно распределенный непустой набор объектов с конкретным типом объекта-члена, называемым типом элемента. (N1570 осадка для C11, 6.2.5 типов §20). Таким образом, при условии, что мы можем убедиться, что члены являются последовательными (то есть между ними нет отступов), было бы законно рассматривать это как массив.
Вот упрощенный пример, который компилируется без предупреждения и дает ожидаемые результаты во время выполнения:
#include <stdio.h>
#include <stddef.h>
#include <assert.h>
struct quad {
int x;
int y;
int z;
int t;
};
int main() {
// ensure members are consecutive (note 1)
static_assert(offsetof(struct quad, t) == 3 * sizeof(int),
"unexpected padding in quad struct");
struct quad q;
int *ix = &q.x;
for(int i=0; i<4; i++) {
ix[i] = i;
}
printf("Quad: %d %d %d %d\n", q.x, q.y, q.z, q.t);
return 0;
}
Это на самом деле не имеет смысла, но я уже видел пример из реальной жизни, где итерации между членами структуры позволяют более простой код с меньшим риском опечатки.
Вопрос:
В приведенном выше примере это static_assert
достаточно, чтобы сделать легальным псевдоним структуры с массивом?
(примечание 1) Поскольку структура описывает последовательно распределенный непустой набор объектов- членов, более поздние члены должны иметь увеличивающиеся адреса. Просто компилятор может включать отступы между ними. Так что смещение последнего члена (здесь t
) если 3 раза sizeof(int)
плюс общее заполнение перед ним. Если смещение точно 3 * sizeof(int)
тогда в структуре нет заполнения
Вопрос, предложенный в виде дубликата, содержит как принятый ответ, который позволяет думать, что это будет UB, так и ответ +1, который позволяет думать, что он может быть законным, потому что я мог гарантировать, что не существует никакого дополнения.
5 ответов
Я буду спорить UB. Прежде всего, обязательная цитата из 6.5.6 Аддитивные операторы:
Когда выражение с целочисленным типом добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд-указатель указывает на элемент объекта массива, а массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов полученного и исходного элементов массива равна целочисленному выражению. Другими словами, если выражение P указывает на i-й элемент объекта массива, выражения (P)+N (эквивалентно, N+(P)) и (P)-N (где N имеет значение n) указывают соответственно i+n-му и in-му элементам массива, если они существуют. Более того, если выражение P указывает на последний элемент объекта массива, выражение (P)+1 указывает один за последним элементом объекта массива, а если выражение Q указывает на один последний элемент последнего элемента массива, выражение (Q)-1 указывает на последний элемент объекта массива. Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива, оценка не должна вызывать переполнение; в противном случае поведение не определено. Если результат указывает на один последний элемент массива, он не должен использоваться в качестве операнда оцениваемого унарного оператора *.
Я подчеркнул, что я считаю суть дела. Вы правы, когда говорите, что объект массива - это "непрерывно распределенный непустой набор объектов с конкретным типом объекта-члена, называемым типом элемента". Но верно ли обратное? Является ли последовательно распределенный набор объектов объектом массива?
Я собираюсь сказать нет. Объекты должны быть явно созданы.
Итак, для вашего примера, нет объекта массива. Как правило, существует два способа создания объектов в C. Объявите их с автоматическим, статическим или локальным потоком. Или распределите их и дайте хранилищу эффективный тип. Вы не сделали ни для создания массива. Это делает арифметику официально неопределенной.
Нет, псевдоним struct
и массив, как это, он нарушает строгий псевдоним. Обходной путь - обернуть структуру в объединение, которое содержит как массив, так и отдельные члены:
union something {
struct quad {
int x;
int y;
int z;
int t;
};
int array [4];
};
Это уклоняется от строгого нарушения псевдонимов, но у вас все еще могут быть байты заполнения. Что вы можете обнаружить с помощью статического утверждения.
Еще одна проблема остается, и это то, что вы не можете использовать арифметику указателя на int*
указывая на первый член структуры, по разным неясным причинам, описанным в указанном поведении аддитивных операторов - они требуют, чтобы указатель указывал на тип массива.
Лучший способ уклониться от всего этого - просто использовать член массива вышеупомянутого объединения. Это вместе со статическим утверждением приводит к четкому, надежному и переносимому коду.
(Теоретически, вы также можете использовать указатель на символьный тип для перебора структуры - в отличие от int*
это будет разрешено в соответствии с 6.3.2.3/7. Но это более грязное решение, если вас не интересуют отдельные байты.)
Проблема здесь в том, что вы определяете непрерывное распределение: "мы можем убедиться, что члены являются последовательными (то есть между ними нет отступов)".
Хотя это является следствием непрерывного распределения, оно не определяет свойство.
Члены вашей структуры - это отдельные переменные с автоматической продолжительностью хранения, в определенном порядке с заполнением или без него, в зависимости от того, как вы можете управлять своим компилятором, вот и все. Таким образом, вы не можете использовать арифметику указателей для достижения одного члена с учетом адреса другого, и поведение при этом не определено.
Это было бы UB. Как установлено в этом другом вопросе, static_assert может проверять возможное заполнение соответствующим образом. Так что да, 4 члена структуры действительно распределены последовательно.
Но реальная проблема заключается в том, что последовательное распределение необходимо, но недостаточно для формирования массива. Даже если я не смог найти четкую ссылку на него в стандарте C, объекты не могут перекрываться в течение срока их службы - это более четко объяснено в стандарте C++. Они могут быть членами агрегата (структуры или массива), но агрегаты не могут перекрываться. Это согласуется с ответом на Отчет о дефектах № 017 от 10 декабря 1992 года на C89, который цитирует Антти Хаапала в своем ответе на предложенный дубликат.
Даже если С не имеет new
Заявление, что выделенное хранилище имеет свойство не иметь объявленного типа. Это позволяет динамически создавать объекты в этом хранилище, но время жизни выделенного объекта заканчивается, когда по его адресу создается объект другого типа. Таким образом, даже в выделенной памяти мы не можем иметь одновременно и массив, и структуру.
Согласно ответу Лундина, тип сработает через объединение между массивом и структурой должно работать, потому что (ненормативная) заметка говорит
Если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения повторно интерпретируется как представление объекта в новом типе
и оба типа будут иметь одинаковое представление: 4 последовательных целых числа
Без объединений способ перебора элементов массива был бы на уровне байтов, потому что 6.3.2.3 Conversions/Pointers говорит:
7... Когда указатель на объект преобразуется в указатель на тип символа, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата, вплоть до размера объекта, дают указатели на оставшиеся байты объекта.
char *p = q;
for (i=0; i<4; i++) {
int *ix = (int *) (p + i * sizeof(int)); // Ok: points to the expected int member
*ix = i;
}
Но арифметика указателей на не символьные типы для итерации по элементам структуры является UB просто потому, что отдельные члены структуры не могут быть одновременно членами массива.
Начать с -
квотирование C11
глава §6.5.2.1p2
Выражение постфикса, за которым следует выражение в квадратных скобках
[]
является подписанным обозначением элемента объекта массива. Определение подстрочного оператора[]
в том, чтоE1[E2]
идентично(*((E1)+(E2)))
,...
Что значит ix[i]
оценивает *(ix + i)
, Подвыражение здесь ix + i
, ix
имеет тип pointer to integer
,
Сейчас,
квотирование C11
Глава §6.5.6p7
Для целей этих операторов указатель на объект, который не является элементом массива, ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта в качестве его типа элемента.
Таким образом, мы знаем, что ix
указывает на массив размером один. И даже создание указателя на превышение длины (кроме выключения на единицу) является неопределенным поведением, не говоря уже о разыменовании его.
Что заставляет меня истолковывать то, что на самом деле не разрешено