#c #casting #language-lawyer #c89 #strict-aliasing
Вопрос:
Являются ли явные приведения от char *
к другим типам указателей полностью определенным поведением в соответствии с ANSI C89, если указатель гарантированно соответствует требованиям выравнивания типа, к которому вы применяете приведение? Вот пример того, что я имею в виду:
/* process.c */
void *process(size_t elem_size, size_t cap) {
void *arr;
assert(cap > 5);
arr = malloc(elem_size * cap);
/* set id of element 5 to 0xffffff */
*(long *)((char *)arr elem_size*5) = 0xffffff;
/* rest of the code omitted */
return arr;
}
/* main.c */
struct some_struct { long id; /* other members omitted */ };
struct other_struct { long id; /* other members omitted */ };
int main(int argc, char **argv) {
struct some_struct *s = process(sizeof(struct some_struct), 40);
printf("%lxn", s[5].id);
return 0;
}
Этот код компилируется без предупреждений и работает на моей машине так, как ожидалось, но я не совсем уверен, что такие приведения являются определенным поведением.
Проект C89, раздел 4.10.3 (Функции управления памятью):
Указатель, возвращаемый в случае успешного выделения, соответствующим образом выровнен, чтобы его можно было назначить указателю на объект любого типа, а затем использовать для доступа к такому объекту в выделенном пространстве (до тех пор, пока пространство явно не будет освобождено или перераспределено).
Проект C89, раздел 3.3.4 (Операторы литья):
Указатель на объект или неполный тип может быть преобразован в указатель на другой тип объекта или другой неполный тип. Результирующий указатель может быть недействительным, если он неправильно выровнен для указанного типа. Однако гарантируется, что указатель на объект заданного выравнивания может быть преобразован в указатель на объект того же выравнивания или менее строгого выравнивания и обратно; результат должен сравниваться с исходным указателем.
Это четко указывает, что произойдет, если вы приведете от struct some_struct *
к char *
и обратно struct some_struct *
, но в моем случае код, ответственный за распределение, не имеет доступа к полному определению структуры, поэтому он изначально не может указать тип указателя struct some_struct *
, поэтому я не уверен, что правило все еще применяется.
Если код, который я опубликовал, технически является UB, существует ли другой совместимый со стандартами способ изменения элементов массива без знания их полного типа? Существуют ли реальные имплементации, в которых вы ожидали бы, что он будет делать что-то другое, чем ((struct some_struct *)arr)[5].id = 0xffffff;
?
Комментарии:
1. Подумайте о замене ansi-c тегом языкового адвоката
2. IIRC в арифметике указателей C89 над массивом malloc’d (и, следовательно, индексирование) в любом случае технически был УБ, и никого это не волновало.
3. @n.1.8e9-где мой sharem.: Все, что было бы необходимо для исправления правила «строгого псевдонима», заключалось бы в том, чтобы признать, что (1) правило должно применяться только в том случае, если хранилище используется в качестве нескольких типов в некотором конкретном контексте, который может быть нарисован широко или узко для удобства компилятора, и (2) если компилятор, который смотрит в контексте, по крайней мере, так широко, как для #1, может видеть, что указатель или значение одного типа используется для получения указателя или значения другого, он должен признать, что производный указатель может использоваться для доступа к чему-либо оригинального типа.
4. @n.1.8e9-где моя доля.: Я не думаю, что кто-либо из людей, которые одобрили Стандарт, когда-либо представлял, что это будет воспринято как приглашение для компиляторов быть умышленно слепыми в отношении того, как получаются указатели. Колеса отвалились с бессмысленным обоснованием, приведенным для ответа на DR#028, который был кодифицирован как правило «Эффективного типа», угловые случаи которого не могут быть обработаны должным образом, не прилагая абсурдных усилий и/или не принимая во внимание то, что должно быть полезной оптимизацией. Однако примените приведенные выше правила к примеру DR#028, и он ответил бы на него четко.
Ответ №1:
Этот код компилируется без предупреждений и работает на моей машине так, как ожидалось, но я не совсем уверен, что такие приведения являются определенным поведением.
В общем случае приведения имеют определенное поведение, но это поведение может заключаться в том, что результат не является допустимым указателем. Таким образом, разыменование результата приведения может привести к UB.
Рассматривая только функцию process()
, тогда возможно, что результатом ее оценки (long *)((char *)arr elem_size*5)
является недопустимый указатель. Если и только если он недействителен, попытка использовать его для присвоения значения объекту, на который он гипотетически указывает, приводит к UB.
Тем не менее, main()
конкретное использование этой функции нормально:
- В
process()
, указатель, возвращенныйmalloc
и сохраненный вarr
, соответствующим образом выровнен для astruct some_struct
(и для любого другого типа). - Компилятор должен выбрать размер и макет
struct some_struct
таким образом, чтобы каждый элемент и каждый член каждого элемента массива таких структур был правильно выровнен. - Массивы состоят из смежных объектов типа элементов массива без пробелов (хотя сами типы элементов могут содержать заполнение, если они являются структурами или объединениями).
- Следовательно,
(char *)arr n * sizeof(struct some_struct)
должно быть соответствующим образом выровнено для astruct some_struct
, для любого целогоn
числа, чтобы результат указывал в пределах или сразу за концом выделенной области. Это вычисление тесно связано с вычислениями, связанными с доступом к массивуstruct some_struct
с помощью оператора индексирования. struct some_struct
имеет along
в качестве первого элемента, и этот элемент должен отображаться со смещением 0 от начала структуры. Следовательно, каждый указатель, который соответствующим образом выровнен для astruct some_struct
, также должен быть соответствующим образом выровнен для along
.
Комментарии:
1. «возможно, что результатом его оценки
(long *)((char *)arr elem_size*5)
является недопустимый указатель». Предполагая , что все вызывающие объектыprocess()
будут передаватьelem_size
значениеsizeof(long)
,sizeof(array of longs)
илиsizeof(struct with long as first member)
все еще будет возможность(long *)((char *)arr elem_size*5)
вычисления недопустимого указателя?2. @mio, пожалуйста, прочитайте остальную часть этого ответа, особенно »
main()
конкретное использование этой функции нормально».
Ответ №2:
Определение того, поддерживает ли операция ограничения «сглаживания типов» или нарушает их, требует умения ответить на два вопроса:
- Для целей правил псевдонима, когда область хранения содержит «объект» определенного типа.
- Для целей правил псевдонима-это конкретный доступ, выполняемый «с помощью» выражения lvalue определенного типа.
На ваш вопрос, второй вопрос выше является наиболее актуальной: если указатель или именующее типа T1
используется для получения значения типа T2*
который разыменовывается, чтобы открыть хранилище, доступ иногда может потребоваться рассматриваться для целей сглаживания как бы в исполнении lvalue-выражение типа T1
, и иногда как будто в исполнении одного типа T2
, но стандарт не может предложить какое-либо руководство, к которым интерпретация должна применяться при. Конструкции, подобные вашей, были бы предсказуемо обработаны реализацией, которая не злоупотребляла Стандартом в качестве оправдания бессмысленного поведения, но могла бы быть обработана бессмысленно соответствующими, но тупыми реализациями, которые таким образом злоупотребляют Стандартом.
Авторы C89 не ожидали, что кого-то будут волновать точные границы между конструкциями, поведение которых определяется Стандартом, по сравнению с теми, которые, как ожидалось, все реализации будут обрабатываться одинаково, но которые на самом деле не определены Стандартом, и поэтому не видели необходимости определять термины «объект» и «by» с достаточной точностью, чтобы однозначно ответить на вышеуказанные вопросы таким образом, чтобы во всех разумных случаях определялось поведение программы.