Я наткнулся на следующий код:
#include <bitset>
#include <iostream>
int main() {
int x = 8;
void *w = &x;
bool val = *reinterpret_cast<const unsigned char*>(&x);
bool *z = static_cast<bool *>(w);
std::cout << "z (" << z << ") is " << *z << ": " << std::bitset<8>(*z) << "\n";
std::cout << "val is " << val << ": " << std::bitset<8>(val) << "\n";
}
С -O3 это произвело вывод:
z (0x7ffcaef0dba4) is 8: 00001000
val is 1: 00000001
Однако с -O0 это произвело вывод:
z (0x7ffe8c6c914c) is 0: 00000000
val is 1: 00000001
Я знаю, что разыменование z
вызывает неопределенное поведение, и поэтому мы видим противоречивые результаты. Однако кажется, что разыменование reinterpret_cast
в val
не вызывает неопределенного поведения и надежно создает значения {0,1}.
Через (https://godbolt.org/z/f6s11Kr96) мы видим, что gcc для x86 выдает:
lea rax, [rbp-16]
movzx eax, BYTE PTR [rax]
test al, al
setne al
mov BYTE PTR [rbp-9], al
Эффект инструкций test
setne
заключается в преобразовании значений, отличных от 0, в 1 (и сохранении значений 0 в 0). Есть ли какое-то правило, которое гласит, что reinterpret_cast
переход из void *
в const unsigned char *
должен иметь такое поведение?
Это неопределенное поведение.
Согласно -fsanitize=undefined
:
/app/example.cpp:9:41: runtime error: load of value 8, which is not a valid value for type 'bool'
/app/example.cpp:9:70: runtime error: load of value 8, which is not a valid value for type 'bool'
https://godbolt.org/z/vM5MxT4Md
В частности, -fsanitize=undefined
жалуется, что bool
должно содержать либо true
, либо false
, а все остальное — UB. (Я считаю, что битовое представление true
и false
определяется реализацией.)
Да, разыменование z
не определено. Из того, что я видел, компиляторы ожидают, что логические значения будут иметь битовые представления 0, 1 . Это предположение правильно, если исходный код не нарушает систему типов (как это сделано здесь). Однако от reinterpret_cast
до const unsigned char *
значения возвращаются к 0, 1, и мне было интересно, что это за хитрость.
Приведение к необработанным байтам, я думаю, в порядке. Обычно reinterpret_cast
так легко облажаться, что я избегаю этого, когда могу. Гораздо проще безопасно делать подобные вещи с memcpy
(а теперь и с std::bitcast
), и компилятор уберет это.
Что касается expr.reinterpret.cast
reinterpret_cast<T>(v)
Указатель объекта может быть явно преобразован в указатель объекта другого типа.58 Когда prvalue v типа указателя объекта преобразуется в тип указателя объекта «указатель на cv
T
», результатом являетсяstatic_cast<cv T*>(static_cast<cv void*>(v))
.[Примечание 6: Преобразование указателя типа «указатель на
T1
», указывающего на объект типаT1
, в тип «указатель наT2
» (гдеT2
— тип объекта, а требования к выравниваниюT2
не строже, чем требованияT1
) и возврат к исходному типу дает исходное значение указателя. — примечание в конце].
Выражение bool val = *reinterpret_cast<const unsigned char*>(&x);
является допустимым кодом.
Чтение значения *z
вызывает неопределенное поведение из-за строгого нарушения алиасинга (C++20 [basic.lval]/11) . Выражение имеет тип bool
, но объект в ячейке памяти имеет тип int
. Псевдонимы разрешены только для определенных пар типов, и bool to int не является одним из них.
Часть кода val
не является UB, потому что const unsigned char
разрешено использовать псевдонимы для других типов. Инициализатор для val
создаст значение unsigned char
, представление памяти которого совпадает с содержимым первого байта x
.
Затем этот результат преобразуется (не переинтерпретируется) в bool
, производя либо false
, если 0, либо true
в противном случае.
Доступ (то есть чтение) значения z
(а не просто разыменование самого себя) вызывает неопределенное поведение, потому что это нарушение псевдонимов. (z
указывает на объект типа int
, но доступ осуществляется через lvalue типа bool
)
Доступ через lvalue типа unsigned char
специально освобождается от нарушения псевдонимов. (см. [basic.lval]/11.3)
Однако технически до сих пор не указано, каким должен быть результат доступа к int
объекту через unsigned char
lvalue. Намерение состоит в том, что он дает первый байт объектного представления объекта int
, но в настоящее время стандарт не соответствует этому поведению. В статье P1839 делается попытка устранить этот недостаток.
Прочитав этот первый байт из представления объекта как значение unsigned char
, вы неявно преобразуете его в bool
при инициализации bool val
из него. Преобразование из unsigned char
в bool
— это преобразование значений, а не переосмысление представления объекта. Указано, что нулевое значение преобразуется в false
, а все остальное — в true
. (см. [conv.bool])
Приводите ли вы через void*
явно или напрямую приводите int*
к unsigned char*
или bool*
, вообще не имеет значения. reinterpret_cast
между указателями фактически указывается как эквивалент static_cast<void*>
, за которым следует static_cast
для целевого типа указателя. (В вашем коде static_cast
и reinterpret_cast
взаимозаменяемы.)
Спасибо за все эти подробности! Таким образом, (после P1839) мы должны ожидать, что val
будет истинным, если первый байт x
отличен от нуля, и ноль в противном случае (например, 0, 256).
@byrnesj1 Да, на практике это всегда так. Конечно, то, как значение объекта int
относится к значению первого байта в представлении объекта, определяется реализацией и обычно зависит, по крайней мере, от порядка байтов. Также возможно, например. для int
иметь биты, которые не участвуют в представлении значения. В этом случае может быть невозможно предсказать результат по значению int
.
@byrnesj1 Например, в типичной архитектуре с обратным порядком байтов не имеет значения, какое значение от 0 до 256 (и выше, пока вы не перейдете к старшему байту) вы выберете. Все они приведут к false
. Только старшие биты будут иметь значение.
Конверсии
signed <-> unsinged
иsmall type <-> big type
— задачаstatic_cast
:bool val = static_cast<const unsigned char>(x);
.