Часто говорят, что тот или иной код невалиден, так как он нарушает «strict aliasing». А что это такое?
Ответ
Aliasing (псевдонимы/наложение/алиасинг) - это ситуация, когда два разных имени (например указателя) обозначают один и тот же объект.
int x;
int* p = &x;
int& r = x;
// алиасинг: x, r и *p обозначают один и тот же объект.
Это важно для оптимизатора: если есть два однотипных указателя, то после записи в один указатель, значение по другому указателю может измениться:
int f(int* a, int* b) {
*a = 0;
*b = 1;
return *a; // в *a может быть как 0 так и 1,
// оптимизатор не может использовать return 0
}
Strict aliasing (строгий алиасинг) - это неофициальное название правила, согласно которому алиасинг запрещен для разнотипных объектов.
В стандарте С++ это правило звучит следующим образом:
3.10 Lvalues and rvalues [basic.lval] параграф 10:
Если программа пытается получить доступ ко значению объекта через glvalue типа, который не перечислен в списке ниже, то поведение не определено:
динамический тип этого объекта, в т.ч. с добавлением const/volatile или signed/unsigned
тип, похожий на динамический тип объекта (например const int* похож на int*, см. 4.4. [conv.qual]);
агрегатный тип (массив или класс или union), который включает в себя член данных с одним из типов, указанных выше;
базовый тип динамического типа объекта (в т.ч. с добавлением const/volatile);
char или unsigned char
(Сноска: цель этого списка - указать случаи, когда алиасинг разрешен)
Объект - это область памяти. У него есть время жизни, тип и может быть имя.
Динамический тип - это тип наиболее унаследованого объекта, на который указывает выражение. Например если D наследуется от B, и есть переменная B* b = new D;, то динамический тип *b - это D
Из этого следует, что хотя указатель на один тип можно преобразовать в указатель на другой тип, из получившегося указателя нельзя ничего читать:
char* pc = new char[100];
int* pi = reinterpret_cast
Оптимизатор может использовать это следующим образом:
int f(int* a, short* b) {
*a = 0;
*b = 1;
return *a; // в *a может быть только 0,
// у *b другой тип, по этому запись в *b не может менять *a
// оптимизатор может изменить код на return 0
}
Пункт про массивы и классы означает, что к объекту можно обратиться через объект в котором он находится, например:
struct S {
int a;
};
S s;
s.a = 1;
S s_ = s; // Доступ к S::a через весь объект с типом S (довольно очевидно)
При этом, код с использованием другого типа не валиден не из-за strict aliasing, а из-за попытки разыменования указателя, полученного в результате reinterpret_cast. Стандарт разрешает только обратное преобразование (впрочем, тут используется термин unspecified, так что компилятор может использовать свои правила).
struct S2 {
int a;
};
S2* s2 = reinterpret_cast
Для union определено понятие активного члена данных, поэтому чтение другого члена нарушает strict aliasing:
union U {
int i;
short s;
char c;
};
U u;
u.i = 0; // активный член
short s = u.s; // ЗАПРЕЩЕНО, обращение к объекту с типом int через тип short
char c = u.c; // ОК, char - это особый случай
Последний пункт списка про char или unsigned char - это лазейка для функций вида memcpy/memset/etc:
void my_zero_memory(void* p, size_t n) {
char* bytes = static_cast
int x[100];
my_zero_memory(x, sizeof(x));
Однако любые попытки использовать другие типы приводят к неопределенному поведению, например:
// НЕПРАВИЛЬНО
void my_fast_zero_memory(void* p, size_t n) {
uint64_t* quads = static_cast
К сожалению, в интернете полно такого "быстрого" кода, который в любой момент может сломаться если компилятор использует какую-нибудь оптимизацию после встраивания такой функции. (Правильный memset - это стандартный memset, или его надо писать например на ассемблере, где нет правил strict aliasing).
Поскольку такого неправильно написанного кода очень много, то в компиляторе GCC есть опция -fno-strict-aliasing, которая отключает оптимизации связанные с алиасингом.
Комментариев нет:
Отправить комментарий