Является ли это использование профсоюзов строго соответствующими?
С учетом кода:
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };
static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}
int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
if (read_s1x(&p1->v1))
{
unsigned short temp;
temp = p3->v1.x;
p3->v2.x = temp;
write_s2x(&p2->v2,1234);
temp = p3->v2.x;
p3->v1.x = temp;
}
return read_s1x(&p1->v1);
}
int test2(int x)
{
union s1s2 q[2];
q->v1.x = 4321;
return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
printf("%d\n",test2(0));
}
Во всей программе существует один объект объединения - q
. Его активный член установлен на v1
, а затем на v2
, а затем на v1
снова. Код использует только адрес-оператора на q.v1
или результирующий указатель, когда этот элемент активен, а также q.v2
. Поскольку p1
, p2
и p3
являются одинаковыми, для доступа к p1->v1
и p3->v2
для доступа к p2->v2
должно быть совершенно легально использовать p3->v1
.
Я не вижу ничего, что могло бы оправдать компилятор, неспособный вывести 1234, но многие компиляторы, включая clang и gcc, генерируют код, который выводит 4321. Я думаю, что происходит то, что они решают, что операции на p3 на самом деле не будут изменить содержимое любых бит в памяти, их можно просто игнорировать вообще, но я не вижу ничего в стандарте, который оправдывал бы игнорирование того факта, что p3
используется для копирования данных из p1->v1
в p2->v2
и наоборот.
Есть ли что-нибудь в стандарте, оправдывающее такое поведение, или компиляторы просто не следуют ему?
Ответы
Ответ 1
Я считаю, что ваш код является совместимым, и есть недостаток в режиме -fstrict-aliasing
GCC и Clang.
Я не могу найти нужную часть стандарта C, но такая же проблема возникает при компиляции вашего кода в режиме С++ для меня, и я нашел соответствующие отрывки стандарта С++.
В стандарте С++ [class.union]/5 определяет, что происходит, когда оператор =
используется в выражении доступа к объединению. В стандарте С++ указано, что, когда объединение участвует в выражении доступа к членству встроенного оператора =
, активный член объединения изменяется на член, участвующий в выражении (если тип имеет тривиальный конструктор, но потому что это код C, у него есть тривиальный конструктор).
Обратите внимание, что write_s2x
не может изменить активный член объединения, поскольку объединение не участвует в выражении присваивания. Ваш код не предполагает, что это происходит, поэтому оно ОК.
Даже если я использую место размещения new
для явного изменения члена профсоюза, который должен быть подсказкой для компилятора, который изменил активный член, GCC по-прежнему генерирует код, который выводит 4321
.
Это похоже на ошибку с GCC и Clang, предполагая, что переключение активного члена объединения не может произойти здесь, потому что они не могут распознать возможность p1
, p2
и p3
всех указывать на один и тот же объект.
GCC и Clang (и почти каждый другой компилятор) поддерживают расширение на C/С++, где вы можете прочитать неактивный член объединения (получая все возможное значение мусора в результате), но только если вы сделаете этот доступ в выражение доступа к члену, включающее объединение. Если v1
не был активным членом, read_s1x
не будет определяться поведением в этом конкретном правиле реализации, поскольку объединение не входит в выражение доступа к члену. Но поскольку v1
является активным членом, это не имеет значения.
Это сложный случай, и я надеюсь, что мой анализ будет правильным, так как тот, кто не является компилятором или членом одного из комитетов.
Ответ 2
При строгой интерпретации стандарта этот код может не соответствовать. Позвольте сосредоточиться на тексте известного §6.5p7:
Объект должен иметь сохраненное значение, доступ к которому имеет только выражение lvalue, которое имеет один из следующие типы:
- тип, совместимый с эффективным типом объекта,
- квалифицированная версия типа, совместимая с эффективным типом объекта,
- тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объект,
- тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективный тип объекта,
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди его членов (в том числе, рекурсивно, члена субагрегата или объединенного союза), или
- тип символа.
(акцент мой)
В ваших функциях read_s1x()
и write_s2x()
сделайте напротив того, что я выделил выше, в контексте всего вашего кода. Только в этом параграфе вы можете сделать вывод, что это не разрешено: указателю на union s1s2
будет разрешено псевдоним указателя на struct s1
, но не наоборот.
Эта интерпретация курса означала бы, что код должен работать по назначению, если вы "встроите" эти функции вручную в свой test()
. Это действительно имеет место здесь с gcc 6.2 для i686-w64-mingw32
.
Добавление двух аргументов в пользу приведенной выше строгой интерпретации:
-
В то время как всегда разрешено псевдоним любого указателя с char *
, массив символов не может быть псевдонимом любым другим типом.
-
Учитывая (здесь несвязанный) §6.5.2.3p6:
Для упрощения использования союзов предусмотрена одна специальная гарантия: если соединение содержит несколько структур, которые имеют общую начальную последовательность (см. ниже), и если объединение объект в настоящее время содержит одну из этих структур, разрешено проверять общие начальная часть любого из них где угодно, что объявление завершенного типа объединения.
(опять же мое внимание) - типичная интерпретация заключается в том, что видимость означает непосредственно в области рассматриваемой функции, а не "где-то в единицах перевода"... поэтому эта гарантия не включает функцию, которая принимает указатель на один из struct
, который является членом union
.
Ответ 3
Я не читал стандарт, но играть с указателями в режиме строгого сглаживания (т.е. используя -fstrict-alising
) опасно. См. gcc online doc:
Обратите особое внимание на такой код:
union a_union {
int i;
double d;
};
int f() {
union a_union t;
t.d = 3.0;
return t.i;
}
Практика чтения из другого члена союза, кроме последнего, написанного на (называемая type-punning
), является обычной. Даже при -fstrict-aliasing
допускается использование пула при условии, что доступ к памяти осуществляется через тип объединения. Таким образом, приведенный выше код работает так, как ожидалось. См. Структуры перечислений объединений и реализация битовых полей. Однако этот код может не быть:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
Аналогично, доступ, взяв адрес, набрав результирующий указатель и разыменовывая результат, имеет поведение undefined, даже если cast использует тип объединения, например:
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
Опция -fstrict-aliasing
активируется на уровнях -O2, -O3, -Os.
Нашел что-нибудь подобное во втором примере huh?
Ответ 4
Это не о том, чтобы соответствовать или не соответствовать - это одна из оптимизационных "ловушек". Все ваши структуры данных были оптимизированы, и вы передаете один и тот же указатель на оптимизированные данные, чтобы дерево выполнения было уменьшено до простого значения printf.
sub rsp, 8
mov esi, 4321
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
чтобы изменить его, вам необходимо сделать эту функцию "переноса" боковым эффектом и заставить настоящие задания. Это заставит оптимизатор не уменьшать эти узлы в дереве выполнения:
int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/
main:
sub rsp, 8
mov esi, 1234
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
это довольно тривиальный тест, только искусственно сделанный немного сложнее.