Вот строгое правило псевдонимов в действии: одно из предположений, сделанных компилятором C (или C ++), заключается в том, что разыменование указателей на объекты разных типов никогда не будет ссылаться на одну и ту же ячейку памяти (то есть на псевдонимы друг друга).
Эта функция
int f(struct t1* p1, struct t2* p2);
предполагает, что p1 != p2
, поскольку они формально указывают на разные типы.В результате оптимизатор может предположить, что p2->m = -p2->m;
не влияет на p1->m
;сначала он может прочитать значение p1->m
в регистр, сравнить его с 0, если он сравнивает меньше 0, затем выполнить p2->m = -p2->m;
и, наконец, вернуть значение регистра без изменений!
Объединение здесь - единственный способ сделать p1 == p2
на двоичном уровне, поскольку все члены объединения имеют одинаковый адрес.
Другой пример:
struct t1 { int m; };
struct t2 { int m; };
int f(struct t1* p1, struct t2* p2)
{
if (p1->m < 0) p2->m = -p2->m;
return p1->m;
}
int g()
{
union {
struct t1 s1;
struct t2 s2;
} u;
u.s1.m = -1;
return f(&u.s1, &u.s2);
}
Что необходимоg
вернуть?+1
в соответствии со здравым смыслом (мы меняем -1 на +1 в f
).Но если мы посмотрим на сгенерированную сборку gcc с -O1
оптимизацией
f:
cmp DWORD PTR [rdi], 0
js .L3
.L2:
mov eax, DWORD PTR [rdi]
ret
.L3:
neg DWORD PTR [rsi]
jmp .L2
g:
mov eax, 1
ret
Пока все так же исключено.Но когда мы пробуем это с -O2
f:
mov eax, DWORD PTR [rdi]
test eax, eax
js .L4
ret
.L4:
neg DWORD PTR [rsi]
ret
g:
mov eax, -1
ret
Возвращаемое значение теперь жестко закодировано -1
Это потому, что f
в начале кэширует значение p1->m
в регистре eax
(mov eax, DWORD PTR [rdi]
) и не перечитывает его после p2->m = -p2->m;
(neg DWORD PTR [rsi]
) - возвращает eax
без изменений.
union здесьиспользуется только для Все нестатические члены-данные объекта объединения имеют одинаковый адрес. как результат &u.s1 == &u.s2
.
- кто-то, кто не понимает ассемблерный код, может показать в c / c ++ , как строгие псевдонимы влияют на код f:
int f(struct t1* p1, struct t2* p2)
{
int a = p1->m;
if (a < 0) p2->m = -p2->m;
return a;
}
кэш компилятора * значение 1055 * в локальном vara
(на самом деле в реестре, конечно) и вернуть его, несмотря на p2->m = -p2->m;
изменение p1->m
.но компилятор предполагает, что p1
память не затронута, потому что он предполагает, что p2
указывает на другую память, которая не перекрывается с p1
, поэтому при разных компиляторах и разном уровне оптимизации один и тот же исходный код может возвращать разныезначения (-1 или +1).так и неопределенное поведение как есть