В посте о
reinterpret_cast я имел неаккуратность показать не самый легальный пример, т.к. он приводит к UB. Напомню пример:double *pointer_f64 = new double(42);
int64_t *pointer_i64 = reinterpret_cast<int64_t *>(pointer_f64);
Наложение, пересечение областей памяти с разными типами — это достаточно старое и известное явление, именуемое aliasing. С нашей точки зрения указатели
pointer_f64 и pointer_i64 ссылаются на одну и ту же область памяти, но работают с ней, как с разными типами. И это в моем примере работало, где же тут UB?Давайте посмотрим на этот живой пример. Результат отличается, в зависимости от примененных оптимизаций
-O0 vs -O2. Это и есть то самое неопределенное поведение, но полученное в других условиях. Тут является важным именно то, через какой тип программа получает доступ к пересекающейся области памяти. От этого зависит будет ли компилятор подставлять значения констант или нет.Познакомимся с правилом strict aliasing. Стандарт С++ определяет понятие динамического типа в §3.9:
Динамический тип — это тип наиболее наследованного объекта, который инициализирован в памяти.
В соответствии со стандартом C++ §8.2.1, обращаться к памяти объекта через другой тип легально, если:
1. Если мы меняем динамический тип объекта
void *origin = malloc(sizeof(int));
// placement new инициализирует int
int *other = new (origin) int(0);
2. Если другой тип совпадает с исходным типом
int variable = 123;
int *alias = &variable; // OK
3. Если другой тип — это cv-специфицированный тип динамического типа
int variable = 123;
int const *alias = &variable; // OK
4. Если другой тип — это знаковый или беззнаковый тип динамического типа
unsigned int variable = 123;
signed int *alias = reinterpret_cast<signed int*>(&variable); // OK
5. Если другой тип — это знаковый или беззнаковый cv-специфицированный тип динамического типа
unsigned int variable = 123;
signed int const *alias = reinterpret_cast<signed int*>(&variable); // OK
6. Если другой тип совпадает с динамическим типом члена, который включает в себя агрегатный тип (структура, класс, массив, или union), в том числе вложенные
union U
{
int ivalue;
float dvalue;
};
// OK
auto set_default(U &u, int &ivalue)
{
ivalue = 0;
u.dvalue = 2.0;
return std::pair(ivalue, u.dvalue);
}
Объединение
U содержит тип int. Следовательно, int &ivalue потенциально может перезаписывать пересекающуюся память. Компилятор учтет это и не применит оптимизацию.7. Если другой тип - это родительский тип динамического типа
struct Derived : Base {};
// OK
Derived *derived = new Derived;
Base *base = derived;
8. Если другой тип - это cv-специфицированный тип базового класса динамического типа
Derived *derived = new Derived;
const Base *base = derived;
9. Если другой тип - это char, unsigned char, std::byte, int8_t, uint8_t
int *data = new int();
uint8_t *byte = reinterpret_cast<uint8_t*>(data);
Это является исключением, т.к. требуется в разнородных функциях типа memcpy, memset и т.д.
Во всех остальных случаях, обращение к памяти другого объекта является UB!
Пост получился большой, но сказать на эту тему хотелось бы еще больше: о мотивации, o ключевом слове restrict, опциях сборки. Завтра более подробно расскажу, почему в стандарте появилось это правило strict aliasing и что оптимизирует компилятор.
#cppcore #compiler #optimization