Comments 60
Мы видим, что glibc typedef-ит int8_t и uint8_t для signed char и unsigned char соответственно
но, как всегда, есть нюанс. char это тип, отличный и от unsigned char, и от signed char. Более того, даже знаковость char не определена стандартом.
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.
cppreference. Strict aliasing распространяется на signed char/int8_t!
В cppreference тоже есть кусок с reinterpret_cast:
alignas(Y) std::byte s[sizeof(Y)];
Y* q = new(&s) Y{2};
const int f = reinterpret_cast<Y*>(&s)->z; // Class member access is undefined behavior:
// reinterpret_cast<Y*>(&s) has value "pointer to s"
// and does not point to a Y object
const int g = q->z; // OK
const int h = std::launder(reinterpret_cast<Y*>(&s))->z; // OK
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
добавить лишь reinterpret_cast с
float*
на int*
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
int *p = reinterpret_cast<int*>(f);
*p = 2;
return *i;
}
то будет генерироваться код без strict aliasing.
Таким образом, дополнительная обёртка
reinterpret_cast
'а в std::launder
принципиально ничего не добавит в подобных случаях.foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 2
mov eax, dword ptr [rsi]
ret
Попробуйте оптимизацию повключать, вполне возможно, что результат изменится.
Но даже если нет — это не показатель отсутствия UB.
foo
.То, что сейчас компилятор генерирует код, который делает что-то вменяемое — счастливая случайность.
Допустим, если слегка поменять типы, а архитектуру выбрать ARM Cortex-M3, то можно устроить вот такое:
char array[8] = {0};
double * d = reinterpret_cast<double *>(array);
*d = 8.8;
double dd = *d;
Тут с вероятностью 7/8 ошибка будет уже при записи 8.8 по указателю d, потому что на этой архитектуре доступ к double должен быть выровнен по границе 8 байт.
На мой взгляд, ваш код принципиально ничем не отличается и будет работать просто потому, что конкретная архитектура вам это позволяет сделать.
Но если копнуть, вполне может оказаться, что не всегда. Например, могут найтись такие значения int, которые будут соответствовать некорректным значениям для float (что-нибудь в районе двоичного представления NaN, например), что породит исключение. Или типа того.
Собственно, единственная причина, по которой можно создать такой указатель — нарушение strict aliasing'a :)Ну, не стал бы так категорично утверждать. И в любом случае кошерное обертывание в std::launder в таких случаях (не выровненный доступ) не спасёт.
Ну, не стал бы так категорично утверждать.
А можете пример без нарушения придумать?
И в любом случае кошерное обертывание в std::launder в таких случаях (не выровненный доступ) не спасёт.
Ну, это понятно, да. Видимо, предполагается, что тут программист просто говорит компилятору — «спокуха, я знаю, что делать».
Собственно, единственная причина, по которой можно создать такой указатель — нарушение strict aliasing'a :)Бремя доказательства лежит на утверждающем. Хотя может быть это и так.
Тут с вероятностью 7/8 ошибка будет уже при записи 8.8 по указателю d, потому что на этой архитектуре доступ к double должен быть выровнен по границе 8 байт.
А ничего, что в cortex-M3 нет fpu в принципе? В cortex-M4 фпу бывает, но тоже только single precision.
А вот double читается командой ldrd, которая поддерживает только выровненный доступ. Правда, я слегка приврал, надо, чтобы было выровнено по 4, не по 8, но не суть.
Пруф.
Вот на Cortex-M4F, где есть fpu, уже и float'ы трогать через кривой указатель нельзя, fpu в невыровненный доступ совсем не умеет.
и читается обычной ldr, которая поддерживает невыровненный доступ
Тут еще такой момент, что поддержку невыровненного доступа можно процессору отключить. static.docs.arm.com/ddi0403/ec/DDI0403E_c_armv7m_arm.pdf страница 601
UB возникает, когда программист нарушает некий контракт, на который рассчитывает компилятор (или код, или процессор — не суть).
И компилятор (или код, или процессор) просто продолжает работать, исходя из того, что контракт не нарушался.
Вот, допустим, есть у вас функция, которая принимает на вход нуль-терминированную строку. А вы ей подсунули строку без нуля на конце. И что она будет делать? Проверить, есть ли там нуль нельзя, длина ведь заранее неизвестна. Так что функция просто работает, как будто все в порядке.
Именно поэтому UB открывает дорогу оптимизациям — компилятор получает право делать далеко идущие выводы о том, как будет выполняться код, не проверяя этого ни на этапе компиляции, ни в рантайме. А просто рассчитывая, что программист соблюдает этот самый контракт.
int arr[5];
arr + 1;
По сходным причинам, кстати, union-style cast — тоже UB.UB возникает только когда вы пишите по одному полю, а читаете из другого.
+------+----------+---------+
| size | capacity | data… |
+------+----------+---------+
Пользовательский указатель указывает на data. В ряде функций, связанных с этим вектором необходимо получить size. Для этого необходимо от переданного пользовательского указателя отнять 16 байт и считать/записать size_t. Будет ли нарушать правило strict aliasing следующий код?
((size_t *)(vec))[-2] = size;
Для UB достаточно чтения с 2 указателей разного типа. А откуда инфа, что только в пределах 1 функции?! Или это просто наблюдения нескольких компиляторов. Если второе, то всё плохо. Вон, люди memcpy для заполнения памяти последовательностью использовали. Везде работало, а потом раз и перестало. С UB шутки плохи, ибо оно проявляется неожиданно и может долгое время не давать о себе знать.
Если его не нарушать, то и за выравниванием следить не придется.
Извините, не понимаю вопрос. strict aliasing разрешает (если я правильно помню):
- касты к char * и обратно
- касты к void * и обратно
- касты к signed/unsigned варианту текущего типа
- касты к const варианту текущего типа и обратно
Вроде все. Все остальное — UB. Причем тут "на сколько байт"?
Поколупались — оказалось местный libc стек выравнивает (точных чисел не помню уже, пусть будет 4 и 8) не на 8 а на 4 байта.
Долго не страдали, решили даблы тут не печатать (вот такая платформа, что проще было забить).
вот было у нас пара реальных проектов — перевести кодовую базу из проекта, в котором int был 16-битный на 32-битный. А потом другой проект — с 32-битной архитектуры на 64-битную архитектуру (а там указатели были на это завязаны).
во-вторых, будет у вас либа c ABI отличающемся от ABI принятой в вашем проекте (старая или проприетарная, от которой у вас только только хедер) — тоже легко словить интересное поведение в рантайме.
int foo( volatile float *f, volatile int *i ) {
Теперь компилятор обязан произвести обе записи. Или...?
f и i не должны ссылаться на один и тот же блок памяти, ибо они имеют разный тип. volatile не убирает UB. А UB означает, что может произойти, что угодно.
Почему-то не упомянули атрибут may_alias
(GCC, Clang), который отключает анализ псевдонимов на совместимось типов, как один из самых простых способов произвести необходимые приведения типов без отключения -fstrict-aliasing
:
/* This type lifts strict aliasing restrictions */
typedef
uint64_t __attribute__((__may_alias__))
uint64_aliased_t;
Это пригодилось для реализации быстрого копирования небольших выровненных массивов:
/* Aligned copy example;
* dst and src pointers may have any effective type
* (alignment issues aside)
*/
uint64_aliased_t *dst_ = dst;
uint64_aliased_t const *src_ = src;
for (size_t i = 0; i < size_; ++i)
dst_[i] = src_[i];
Без этих атрибутов копирование, естественно, не работало для всех указателей dst
и src
, у которых эффективный тип был отличен от char
. В частности, компилятор не выполнял копирование в том случае, когда один из указателей был получен в результате сведения (array type decay) массива alignas(8) uint32_t
.
Согласен, если в качестве единственного компилятора используется GCC (или Clang), то собственная реализация копирования памяти в большинстве случаев не потребуется. Но мне было необходимо поддерживать и другие компиляторы, у которых нет inline версии memcpy()
, и для этих компиляторов использование своего копирования позволило увеличить производительность операции почти втрое. А чтобы код был один и тот же везде, для компиляторов, поддерживающих анализ совместимости типов, пришлось добавить атрибуты may_alias
.
Что такое Strict Aliasing и почему нас должно это волновать? Часть 1