Pull to refresh

Comments 24

А что же все-таки делает std::launder?

Вот странно: все белые люди пытаются скрыть особенности (костыли) архитектуры и изолировать сущности разной природы, а C++ упорно делает обратное. Что ведёт к повышению сложности на ровном месте. Мазохизм короче.

ps:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; 
int x = *reinterpret_cast<int*>(arr);

Этот код вообще будет странно работать при 16-битных char и sizeof(int)=2
В таких случаях не является ли панацеей использовать строго определённые размеры типов, вроде псевдонимов из stdint.h?
Нужно же на чем-то писать то, что будет «скрывать особенности архитектуры и изолировать сущности разной природы», необходимость работать с архитектурой и с этими сущностями «разной природы» все равно по большому счету никуда не девается. C и C++ как раз для этого и нужны. Хотите изолироваться по максимуму — пишите на Java или C#.
gcc с использованием флагов -fstrict-aliasing и -Wstrict-aliasing может отлавливать некоторые случаи, хотя и не без ложных срабатываний/неприяностей.
Реальность, к сожалению, такова, что очень много кода ломается при строгом алиасинге разными неочевидными способами. Любую более-менее сложную программу, задействующую какие-либо преобразования типов, не стоит собирать без ключа -fno-strict-aliasing. Во многих операционных системах он включен по умолчанию (часть дефолтных C[XX]FLAGS, например, во FreeBSD), Linux тоже с ним собирается и т.д.
в общем — если вам надо работать над побитовым преобразования типов (или еще чем-то подобным), то:
1) подумайте, как переписать код, чтобы обойтись без него (интересно, как много людей у которых задачи вынуждают писать приведение указателя на флоаты в указатели на инты ?)
2) напишите отдельную функцию на С для вашей платформы, с нужными ключами компиляции и т.д., и линкуйтесь с ней.
Это-то ладно, главная проблема в том, как найти в своём коде все места, где такое преобразование уже сделано?
как много людей, у которых задачи вынуждают писать приведение указателя на флоаты в указатели на инты?
Очень немного. Проблема в том, что большинство вообще не видит проблемы в тайпкастах (любых), они их нимало не напрягают; я уже устал объяснять почему это плохо, и что типы это друзья программиста, насильно везде включать -Wcast-align и пр. Очень рад, что в плюсах синтаксис кастов нарочно сделали таким уродским; надеюсь, в грядущих стандартах C++ сишные касты вообще, наконец, запретят.
Ой, прошу прощения, слегка промахнулся веткой. :-(
Иногда (глядя на очередное неявное преобразование в коде стажера) посещает мысль, что надо сделать такой ключ компилятора, который вообще запрещает все неявные преобразования типов (особенно в булеан) и все тайпкасты.
И принудительно его добавлять его в школьных\студенческих лабах и у стажеров. (ОК, для матолимпиад и кода мидл+ — можно без него)

И чтоб оно автоматически уровень пишущего код определяло!

Да, иногда на C++ может поднять настроение :)
struct A { double x; };
struct B : A { double y; };

void cpp() {
  A* a=new B[2];
  a[1].x=1.0;
  delete[] a;
}

stdсломается, его нельзя использовать без него. Да что там, даже простейшие веши могут являться неявными преобразованиями, только никьо об этом не думает. const char* s = "Hello!" это неявное преобразования. Используете string - уже два.

Мне вот требуется сериализация флоатов в хмл. При том нужна полная точность, как записал так и прочитал, поэтому я не делаю перевод в десятичную запись и обратно, а кастую в (std::uint32_ t)&f, потом пишу его в шестнадцатиричным текстом в хмл. Подскажете более элегантный- strict'ный способ это сделать?

Вообще-то флоаты сериализуются несколько иначе, но если вам портабельность не нужна, то ответ есть в самой статье — memcpy(3) с ассертом, что размеры типов именно такие, как вы ожидаете.
Портабельность мне как раз нужна. Современные компиляторы поддерживают IEEE 754, там флоаты совместимы и в бинарном формате. Во всяком случае мой код успешно обменивается с x86/msvc и arm/clang. Но за ссылку спасибо.
Почему-то всегда думал, что IEEE754 поддерживают прежде всего процессоры, а не компиляторы. В качестве примета — есть такая архитектура VAX, гцц до сих пор ее поддерживает и есть любители-энтузиасты возиться со старым железом. И у них как раз проблема что тамошние флоаты не совместимы с IEEE754.

ieee754 не регулирует порядок байт )

а кастую в (std::uint32_ t)&f

Это UB на ровном месте.

Type aliasing

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined unless one of the following is true:

AliasedType and DynamicType are similar.
AliasedType is the (possibly cv-qualified) signed or unsigned variant of DynamicType.
AliasedType is std::byte (since C++17), char, or unsigned char: this permits examination of the object representation of any object as an array of bytes.

Отсюда. Там дальше разъясняется, что такое «similar», если кратко — uint32_t и float не similar. Вам везет, что у вас это ПОКА работает.
И как же, например, писать бинарные файлы без каста в char*?
An exception to the strict aliasing rules is made for pointers to character types, so it is always OK to inspect an object’s representation via an array of chars. This is necessary to make memcpy-like functions work properly. Извините, лень переводить на русский.

В кэр и в байт можно, при условии что обратно сделаете то же самое.

Strict aliasig rule определяет, какие два различных указателя могут ссылаться на один и тот же объект, а какие — в нормальном случае нет. Когда компилятор видит в обрабатываемом участке кода использование двух различных указателей, для которых согласно правилу допустимо, что они могут указывать на один и тот же участок памяти, то он действует аккуратно, без оптимизаций; в остальных случаях считается, что производится работа с различными объектами и выполняется соответствующая оптимизация генерируемого кода.
На этом собственно основная содержательная история со strict aliasing заканчивается, и к взаимному кастингу указателей она имеет лишь опосредованное отношение.

При этом компилятор действует достаточно умно: например если он видит что программист приводит указатель являющийся входным параметром функции к другому типу, то он переносит этот новый тип на сам входной параметр и компилирует всю функцию как если бы она имела соответствующую сигнатуру. Например если в функции
int foo( float *f, int *i ) {
    *i = 1 ;
    *f = 0.f ;

   return *i ;
}
добавить
int foo( float *f, int *i ) {
    *i = 1 ;
    *f = 0.f ;

   char *c = reinterpret_cast<char*>(i);
   *c = 2;
    
   return *i ;
}
то компилятор будет обрабатывать код как если бы сигнатура была int foo( float *f, char *i ) Т.е. согласно strict aliasing rule эти два указателя — входных параметра могут ссылаться на один и тот же объект.

Ещё не всегда очевидный пример такого же рода:
struct s1 { float d1; };
struct s2 { float d2; float d3; };
struct s3 { char* p3; };

float bar(s1 *p1, s2 *p2, uintptr_t p) {
  p1->d1 = 10;
  p2->d2 = 20;

  s3* r = reinterpret_cast<s3*>(p);
  *r->p3 = 30;

  return p1->d1;
}


Есть также нюанс с перекрытием объектов одного и того же типа при расположении в памяти — частичное перекрытие позволяет с помощью разных указателей (одного и того же типа) по разным относительным смещениям (например с помощью обращения к разным полям структуры) обратиться к одному и тому же участку памяти. В этом случае несмотря на то, что эти два указателя одинакового типа очевидно могут ссылаться на один и тот же объект, и поэтому оптимизация для работающего с ними кода должна быть выключена, факт обращения к разным полям структуры определяется как доступ к строго различным участкам памяти и компилятор оптимизирует данный случай, что приводит к UB.

Не до конца понимаю. Компилятор считает, что char* может перекрываться с чем угодно. Тогда зачем для задачи

Но что, если мы хотим реализовать каламбур массива unsigned char в серию unsigned int и затем выполнить операцию с каждым значением unsigned int? Мы можем использовать memcpy, чтобы превратить массив unsigned char во временный тип unsinged int.

использовать memcpy?

Цитата из https://github.com/alexanius/understanding_strict_aliasing/blob/master/understanding_strict_aliasing.markdown

Символьный тип. Типам char*, signed char*, или unsigned char* спецификация разрешает в особом порядке указывать на всё что угодно. Это значит, что они могут указывать на любую область памяти.

Т.е. компилятор должен предполагать, что unsigned char* и unsigned int* прекрываются и не оптимизировать там ничего

В стандартном Си каламбур через объединение тоже недопустим. Был он допустим в версии Ричи, где объект определялся очень смешно - область памяти определенного размера, занимаемая данными любого типа такого же размера. Гнус по умолчанию такой какламбур поддерживает, а вообще есть понятие активного члена объединения. Только с Си++ добавляется инициализация - создание объекта

Sign up to leave a comment.