reinterpret_cast между char* и std::uint8_t* - безопасно?
Теперь нам всем иногда приходится работать с двоичными данными. В C++ мы работаем с последовательностями байтов, а с самого начала char
был наш строительный блок. Определено, чтобы иметь sizeof
из 1, это байт. И все функции ввода / вывода библиотеки используют char
по умолчанию. Все хорошо, но всегда была небольшая проблема, небольшая странность, которая беспокоила некоторых людей - количество бит в байте определяется реализацией.
Таким образом, в C99 было решено ввести несколько typedef, чтобы разработчики могли легко выражать себя, целочисленные типы фиксированной ширины. Необязательно, конечно, так как мы никогда не хотим навредить переносимости. Среди них, uint8_t
, мигрировал в C++11 как std::uint8_t
8-битный тип беззнакового целого с фиксированной шириной был идеальным выбором для людей, которые действительно хотели работать с 8-битными байтами.
Итак, разработчики приняли новые инструменты и начали создавать библиотеки, которые прямо заявляют, что они принимают 8-битные последовательности байтов, как std::uint8_t*
, std::vector<std::uint8_t>
или иным образом.
Но, возможно, с очень глубоким размышлением, комитет по стандартизации решил не требовать реализации std::char_traits<std::uint8_t>
поэтому запрещаем разработчикам легко и удобно создавать, скажем, std::basic_fstream<std::uint8_t>
и легко читать std::uint8_t
s как двоичные данные. Или, может быть, некоторые из нас не заботятся о количестве бит в байте и довольны этим.
Но, к сожалению, два мира сталкиваются, и иногда вы должны принять данные как char*
и передать его в библиотеку, которая ожидает std::uint8_t*
, Но подождите, вы говорите, не char
переменный бит и std::uint8_t
фиксируется до 8? Это приведет к потере данных?
Ну, есть интересный стандарт на это. char
определено, что оно содержит ровно один байт, а байт - это наименьший адресуемый кусок памяти, поэтому не может быть типа с битовой шириной меньше, чем у char
, Далее определяется, что он может содержать кодовые блоки UTF-8. Это дает нам минимум - 8 бит. Итак, теперь у нас есть typedef, который должен иметь ширину 8 бит, и тип, который имеет ширину не менее 8 бит. Но есть ли альтернативы? Да, unsigned char
, Помните, что подписанность char
определяется реализацией. Любой другой тип? К счастью, нет. Все остальные целочисленные типы имеют требуемые диапазоны, выходящие за пределы 8 бит.
В заключение, std::uint8_t
является необязательным, это означает, что библиотека, которая использует этот тип, не будет компилироваться, если она не определена. Но что, если он компилируется? Я могу с большой степенью уверенности сказать, что это означает, что мы находимся на платформе с 8-битными байтами и CHAR_BIT == 8
,
Как только у нас есть это знание, что у нас есть 8-битные байты, что std::uint8_t
реализуется как либо char
или же unsigned char
Можем ли мы предположить, что мы можем сделать reinterpret_cast
от char*
в std::uint8_t*
и наоборот? Это портативный?
Это то место, где мои стандартные навыки чтения меня подводят. Я читал про безопасно выведенные указатели ([basic.stc.dynamic.safety]
) и, насколько я понимаю, следующее:
std::uint8_t* buffer = /* ... */ ;
char* buffer2 = reinterpret_cast<char*>(buffer);
std::uint8_t buffer3 = reinterpret_cast<std::uint8_t*>(buffer2);
безопасно, если мы не трогаем buffer2
, Поправьте меня если я ошибаюсь.
Итак, учитывая следующие предпосылки:
CHAR_BIT == 8
std::uint8_t
определено.
Это портативно и безопасно лить char*
а также std::uint8_t*
назад и вперед, предполагая, что мы работаем с двоичными данными и потенциальным отсутствием признаков char
не имеет значения?
Буду признателен за ссылки на стандарт с пояснениями.
РЕДАКТИРОВАТЬ: Спасибо, Джерри Коффин. Я собираюсь добавить цитату из стандарта ([basic.lval], §3.10/10):
Если программа пытается получить доступ к сохраненному значению объекта через glvalue, отличный от одного из следующих типов, поведение не определено:
...
- тип char или unsigned char.
РЕДАКТИРОВАТЬ 2: Хорошо, углубляясь. std::uint8_t
не гарантировано быть typedef unsigned char
, Это может быть реализовано как расширенный целочисленный тип без знака, и расширенные целочисленные типы без знака не включены в §3.10/10. Что теперь?
2 ответа
Хорошо, давайте станем действительно педантичными. Прочитав это, это и это, я довольно уверен, что понимаю намерения обоих Стандартов.
Итак, делая reinterpret_cast
от std::uint8_t*
в char*
а затем разыменование результирующего указателя является безопасным и переносимым и явно разрешено [basic.lval].
Тем не менее, делая reinterpret_cast
от char*
в std::uint8_t*
а затем разыменование результирующего указателя является нарушением строгого правила псевдонимов и является неопределенным поведением, если std::uint8_t
реализован в виде расширенного целого типа без знака.
Однако есть два возможных обходных пути, во-первых:
static_assert(std::is_same_v<std::uint8_t, char> ||
std::is_same_v<std::uint8_t, unsigned char>,
"This library requires std::uint8_t to be implemented as char or unsigned char.");
С этим утверждением ваш код не будет компилироваться на платформах, в противном случае это приведет к неопределенному поведению.
Во-вторых:
std::memcpy(uint8buffer, charbuffer, size);
Cppreference говорит, что std::memcpy
обращается к объектам как к массивам unsigned char
так что это безопасно и портативно.
Повторять, чтобы иметь возможность reinterpret_cast
между char*
а также std::uint8_t*
и работать с полученными указателями переносимо и безопасно на 100% в соответствии со стандартом, должны выполняться следующие условия:
CHAR_BIT == 8
,std::uint8_t
определено.std::uint8_t
реализуется какchar
или жеunsigned char
,
С практической точки зрения, вышеупомянутые условия выполняются на 99% платформ, и, скорее всего, нет платформы, на которой первые 2 условия выполняются, а 3-е ложно.
Если uint8_t
существует вообще, по сути, единственный выбор в том, что это typedef для unsigned char
(или же char
если это окажется без знака). Ничто (кроме битового поля) не может представлять меньше памяти, чем char
и единственный другой тип, который может быть размером до 8 бит, это bool
, Следующий наименьший нормальный целочисленный тип short
, который должен быть не менее 16 бит.
Как таковой, если uint8_t
существует на самом деле, у вас действительно есть только две возможности: вы либо кастуете unsigned char
в unsigned char
или кастинг signed char
в unsigned char
,
Первое - это преобразование идентичности, поэтому очевидно безопасное. Последний подпадает под "особую диспенсацию", предоставленную для доступа к любому другому типу как последовательность символов char или unsigned char в §3.10/10, поэтому он также дает определенное поведение.
Поскольку это включает в себя как char
а также unsigned char
приведение к нему как последовательности символов также дает определенное поведение.
Редактировать: Что касается упоминания Люком о расширенных целочисленных типах, я не уверен, как вам удастся применить его, чтобы получить разницу в этом случае. C++ относится к стандарту C99 для определений uint8_t
и так, поэтому цитаты на протяжении оставшейся части этого берутся из C99.
§6.2.6.1 / 3 указывает, что unsigned char
должен использовать чисто двоичное представление, без битов заполнения. Дополняющие биты разрешены только в 6.2.6.2/1, что специально исключает unsigned char
, В этом разделе, однако, подробно описывается чисто двоичное представление - буквально в бит. Следовательно, unsigned char
а также uint8_t
(если он существует) должен быть представлен одинаково на уровне битов.
Чтобы увидеть разницу между ними, мы должны утверждать, что некоторые конкретные биты, если рассматривать их как один, будут давать результаты, отличные от тех, которые рассматриваются как другие, несмотря на тот факт, что оба должны иметь идентичные представления на уровне битов.
Проще говоря: разница в результатах между ними требует, чтобы они интерпретировали биты по-разному - несмотря на прямое требование, чтобы они интерпретировали биты одинаково.
Даже на чисто теоретическом уровне этого трудно достичь. Что-либо, приближающееся к практическому уровню, это очевидно смешно.