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

# include <stdio.h>
int main()
{
    char p[]={0x01,0x02,0x03,0x04};
    int *q = p;
    printf("%x",*q);
    return 0;
}

У меня вопрос, законна ли следующая версия приведенного выше кода? Я совершенно не уверен в преобразовании указателя на char в указатель на объединение, содержащее массив char. Множество вопросов о каламбурах типов здесь, в SO, но я не нашел дубликата, который охватывает использование указателя таким образом.

#include <stdio.h>
#include <stdint.h>

union char_int {
    char p[4];
    int32_t q;
};

int main()
{
    char p[]={0x01,0x02,0x03,0x04};
    int *q = &(((union char_int *)p)->q);
    printf("%x",*q);
    return 0;
}

В связи с этим, я считаю, что эти байты сформируют допустимое значение int32_t для всех возможных представлений, разрешенных стандартом, но если кто-то может подтвердить эту дополнительную деталь, это тоже было бы здорово.

1
hyde 6 Окт 2019 в 20:36
Поведение (union char_int *)p в целом не определяется стандартом C из-за C 2018 6.3.2.3 7: «Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен для указанного типа, поведение не определено ... «если p оказывается выровненным, как необходимо для union char_int, то стандарт говорит:« при повторном преобразовании, результат должен сравниваться с исходным указателем ». В стандарте не говорится, что этот указатель на самом деле имеет какое-либо значение, которое работает как union char_int * каким-либо иным образом.
 – 
Eric Postpischil
6 Окт 2019 в 20:52
Другими словами, если у нас есть union char_int *x = (union char_int *) p;, и это успешно, потому что выравнивание работает, стандарт ничего не говорит о значении x, кроме (char *) x производит то, что сравнивается с p. Значение x не обязательно является допустимым адресом, иначе *x может относиться к совершенно другой памяти, чем p, например.
 – 
Eric Postpischil
6 Окт 2019 в 20:57
На самом деле это не вопрос законности или незаконности, а скорее неопределенное поведение. Первый - приводит к неопределенному поведению из-за нарушения строгого алиасинга. Оба приводят к неопределенному поведению из-за того, что на значение *q влияет базовое целочисленное представление реализации (в основном порядок байтов, но потенциально платформа может не использовать дополнение до двух). И, как указано выше, оба значения не определены из-за алигментации.
 – 
Graeme
6 Окт 2019 в 20:58
1
@Graeme: вариации из-за порядка байтов определяются реализацией, а не неопределенными. Стандарт требует, чтобы реализации документировали свои представления в памяти, в C 2018 6.2.6.1 2: «За исключением битовых полей, объекты состоят из непрерывных последовательностей из одного или нескольких байтов, количество, порядок и кодировка которых либо указаны явно. или определяется реализацией ". После проблем с преобразованием указателя проблема заключается в алиасинге, а не в представлении.
 – 
Eric Postpischil
6 Окт 2019 в 21:18
Связано ли это, представляю ли я, или был ли какой-то язык об адресе структуры, которая может быть приведена к адресу ее первого типа элемента? Конечно, здесь у нас есть union, и мы все равно не приводим from его адрес, так что это ничего не говорит об этом случае.
 – 
hyde
6 Окт 2019 в 22:07

1 ответ

Лучший ответ

Значение фразы «Объект должен иметь свое сохраненное значение, доступ к которому может получить только выражение lvalue, которое имеет один из следующих типов ...» зависит от того, как определяются слова «объект» и «по», используемые в этом правиле. Насколько я могу судить, никогда не было ничего похожего на консенсус по поводу того, что означают эти слова, за исключением того факта, что авторы Стандарта предположительно ожидали, что реализации будут пытаться разумно интерпретировать правило. Обратите внимание, что при буквальном толковании правила это примерно так:

short volatile x;
int test(void)
{
  int y = x+1;
  return y;
}

Вызовет UB, потому что время жизни y начинается, когда код входит в test, что, в свою очередь, происходит до чтения x, но он не может получить значение до тех пор, пока не будет прочитан x. Следовательно, значение y должно измениться в течение его времени существования, но такое действие не требует выражения lvalue типа int или любого другого допустимого типа.

Ясно, что такая интерпретация была бы абсурдной, но на правило, исключающее простые случаи из предположения, что реализации будут знать, что делать, нельзя полагаться при рассмотрении более сложных. Что касается рассматриваемой конструкции, некоторые компиляторы сказали бы, что в выражении lvalue, таком как someUnion.member = 23;, объект union модифицируется "выражением lvalue someUnion", но не обязательно допускают возможность того, что к такому объекту можно получить доступ в другом месте с помощью lvalue типа члена или lvalue других типов объединения, содержащих тот же член. Однако без какой-либо ясности в отношении того, что должно означать слово «by», на самом деле невозможно охарактеризовать какую-либо конкретную интерпретацию как правильную или неправильную.

1
supercat 10 Окт 2019 в 00:24
Правило строгого псевдонима является прямым, в выражении int y = x+1; нет строгого нарушения псевдонима, поскольку к сохраненному значению объекта y не обращается тип, не разрешенный правилом. Разговор о времени жизни и о том, когда переменная начинает свое время жизни и когда она назначается, - это ваша интерпретация, стандарт не говорит, что они связаны, и я не понимаю, как ваш пример - это UB. Кажется, ваше объяснение устанавливает несуществующую связь между временем жизни и строгим алиасингом, но я буду рад узнать что-то новое, если вы можете добавить какое-то объяснение.
 – 
izac89
13 Окт 2019 в 09:05
@ user2162550: Правило в том виде, в котором оно написано, запрещает доступ не только для lvalue других типов, но и для всего, что не является lvalue надлежащего типа. Правильным исправлением было бы ограничить правило объектами, к которым ранее осуществлялся доступ в том же контексте, что и доступ lvalue, и потребовать, чтобы lvalue, используемое для доступа, было явно связано с более ранним объектом в этом контексте. При выполнении такого выражения, как someUnion.arrayMember[i]=x;, доступ осуществляется с помощью lvalue, которое явно является производным от someUnion, но я бы не сказал, что доступ к нему осуществляется "через" lvalue someUnion.
 – 
supercat
13 Окт 2019 в 20:53
@ user2162550: Я не думаю, что кто-то стал бы утверждать перед комитетом C89, что авторы компилятора должны иметь право умышленно игнорировать действия, которые формируют указатель на объект, и немедленно использовать его в контексте его формирования, но ни clang, ни gcc не будут надежно обрабатывать такие операции, кроме случаев использования оператора []. Даже (*((someUnion.arrayMember)+(i))) не распознается как доступ к someUnion или другим его членам, хотя это выражение является само определением someUnion.arrayMember[i] [буквально!].
 – 
supercat
13 Окт 2019 в 21:01
Вы говорите, что (*((someUnion.arrayMember)+(i))) не распознается как доступ к некоторому Союзу или другим его членам, не могли бы вы поделиться соответствующим разделом для этого? Вы имеете в виду, что часть someUnion.arrayMember внутри (*((someUnion.arrayMember)+(i))) не является lvalue? это новость для меня
 – 
izac89
15 Окт 2019 в 18:45
@ user2162550: Ни clang, ни gcc не распознают (*((someUnion.arrayMember)+(i))) как доступ к someUnion. Выражение someUnion.arrayMember является l-значением, но когда оно является левым оператором оператора [] или +, оно раскладывается на значение указателя "не-l", которое идентифицирует первый элемент. массива, а не доступ к объекту.
 – 
supercat
15 Окт 2019 в 22:22