Семантика reinterpret_cast<const unsigned char*>

Я наткнулся на следующий код:

#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

Эффект инструкций testsetne заключается в преобразовании значений, отличных от 0, в 1 (и сохранении значений 0 в 0). Есть ли какое-то правило, которое гласит, что reinterpret_castпереход из void * в const unsigned char * должен иметь такое поведение?

Конверсии signed <-> unsinged и small type <-> big type — задача static_cast: bool val = static_cast<const unsigned char>(x);.

273K 09.02.2023 01:15
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
75
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Это неопределенное поведение.

Согласно -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, и мне было интересно, что это за хитрость.

byrnesj1 09.02.2023 01:30

Приведение к необработанным байтам, я думаю, в порядке. Обычно reinterpret_cast так легко облажаться, что я избегаю этого, когда могу. Гораздо проще безопасно делать подобные вещи с memcpy (а теперь и с std::bitcast), и компилятор уберет это.

Ben 10.02.2023 02:40

Что касается expr.reinterpret.cast

reinterpret_­cast<T>(v)

  1. Указатель объекта может быть явно преобразован в указатель объекта другого типа.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 09.02.2023 02:13

@byrnesj1 Да, на практике это всегда так. Конечно, то, как значение объекта int относится к значению первого байта в представлении объекта, определяется реализацией и обычно зависит, по крайней мере, от порядка байтов. Также возможно, например. для int иметь биты, которые не участвуют в представлении значения. В этом случае может быть невозможно предсказать результат по значению int.

user17732522 09.02.2023 02:24

@byrnesj1 Например, в типичной архитектуре с обратным порядком байтов не имеет значения, какое значение от 0 до 256 (и выше, пока вы не перейдете к старшему байту) вы выберете. Все они приведут к false. Только старшие биты будут иметь значение.

user17732522 09.02.2023 02:31

Другие вопросы по теме