std :: bit_cast с std :: array

14

В своем недавнем выступлении « Обработка типов в современном C ++» Тимур Думлер сказал, что std::bit_castего нельзя использовать для floatпреобразования битов в unsigned char[4]массив, потому что массивы в стиле C не могут быть возвращены из функции. Мы должны либо использовать, std::memcpyлибо подождать до C ++ 23 (или позже), когда что-то вроде reinterpret_cast<unsigned char*>(&f)[i]станет хорошо определенным.

В C ++ 20, мы можем использовать std::arrayс std::bit_cast,

float f = /* some value */;
auto bits = std::bit_cast<std::array<unsigned char, sizeof(float)>>(f);

вместо массива в стиле C, чтобы получить байты float?

Evg
источник

Ответы:

15

Да, это работает на всех основных компиляторах, и, насколько я могу судить по стандарту, он переносим и гарантированно работает.

Прежде всего, std::array<unsigned char, sizeof(float)>гарантированно будет агрегат ( https://eel.is/c++draft/array#overview-2 ). Из этого следует, что он содержит ровно sizeof(float)число chars внутри (обычно как a char[], хотя на самом деле стандарт не предписывает эту конкретную реализацию - но он говорит, что элементы должны быть смежными) и не может иметь никаких дополнительных нестатических элементов.

Поэтому его легко копировать, и его размер соответствует размеру float.

Эти два свойства позволяют вам bit_castмежду ними.

Тимур Доумлер
источник
3
Обратите внимание, что struct X { unsigned char elems[5]; };удовлетворяет правило, которое вы цитируете. Конечно, он может быть инициализирован до 4 элементов. Это может также быть инициализировано списком с 5 элементами. Я не думаю, что какой-либо разработчик стандартной библиотеки ненавидит людей настолько, чтобы это делать, но я думаю, что это технически соответствует.
Барри
Спасибо! - Барри, я не думаю, что это правильно. Стандарт гласит: «может быть инициализирован списком, содержащим до N элементов». Моя интерпретация заключается в том, что «до» подразумевает «не более чем». Что означает, что вы не можете сделать elems[5]. И в этот момент я не вижу, как вы могли бы получить совокупность, где sizeof(array<char, sizeof(T)>) != sizeof(T)?
Тимур Думлер
Я полагаю, что цель правила («агрегат, который может быть инициализирован списком ...») состоит в том, чтобы разрешить либо struct X { unsigned char c1, c2, c3, c4; };или struct X { unsigned char elems[4]; };- так что, хотя символы должны быть элементами этого агрегата, это позволяет им быть либо непосредственными элементами агрегата или элементы одного субагрегата.
Тимур Домлер
2
@Timur "до" не подразумевает "не более чем". Точно так же, как подразумевается, P -> Qничего не подразумевает случай, когда!P
Барри
1
Даже если агрегат не содержит ничего, кроме массива ровно из 4 элементов, нет гарантии, что arrayсам по себе не будет дополнения. Реализации этого могут не иметь заполнения (и любые реализации, которые делают, должны считаться нефункциональными), но нет никакой гарантии, что arrayсам не будет.
Николь Болас
6

Принятый ответ неверен, поскольку не учитывает вопросы выравнивания и заполнения.

По [массиву] / 1-3 :

Заголовок <array>определяет шаблон класса для хранения последовательностей фиксированного размера объектов. Массив является смежным контейнером. Экземпляр array<T, N>хранит Nэлементы типа T, так что size() == Nэто инвариант.

Массив - это агрегат, который может быть инициализирован списком с N элементами, в которые можно преобразовать типы T.

Массив удовлетворяет всем требованиям контейнера и обратимого контейнера ( [container.requirements]), за исключением того, что созданный по умолчанию объект массива не является пустым и этот обмен не имеет постоянной сложности. Массив удовлетворяет некоторым требованиям контейнера последовательности. Описания предоставлены здесь только для операций над массивом, которые не описаны ни в одной из этих таблиц, и для операций, в которых имеется дополнительная семантическая информация.

Стандарт на самом деле не требует std::arrayналичия точно одного общедоступного элемента данных типа T[N], поэтому теоретически это возможно, что sizeof(To) != sizeof(From)или is_­trivially_­copyable_­v<To>.

Я буду удивлен, если на практике это не сработает.

LF
источник
2

Да.

Согласно документу , описывающему поведение std::bit_castи предлагаемую реализацию, поскольку оба типа имеют одинаковый размер и легко копируются, приведение должно быть успешным.

Упрощенная реализация std::bit_castдолжна выглядеть примерно так:

template <class Dest, class Source>
inline Dest bit_cast(Source const &source) {
    static_assert(sizeof(Dest) == sizeof(Source));
    static_assert(std::is_trivially_copyable<Dest>::value);
    static_assert(std::is_trivially_copyable<Source>::value);

    Dest dest;
    std::memcpy(&dest, &source, sizeof(dest));
    return dest;
}

Так как число с плавающей точкой (4 байта) и массив unsigned charс size_of(float)относительно всех тех , утверждает, лежащий в основе std::memcpyбудет осуществляться. Следовательно, каждый элемент в результирующем массиве будет одним последовательным байтом с плавающей точкой.

Чтобы доказать это поведение, я написал небольшой пример в Compiler Explorer, который вы можете попробовать здесь: https://godbolt.org/z/4G21zS . Число с плавающей запятой 5.0 должным образом хранится в виде массива bytes ( Ox40a00000), который соответствует шестнадцатеричному представлению этого числа с плавающей запятой в Big Endian .

Мануэль Гиль
источник
Вы уверены, что std::arrayгарантированно не будет битов заполнения и т. Д.?
LF
1
К сожалению, сам факт того, что некоторый код работает, не означает, что в нем нет UB. Например, мы можем написать auto bits = reinterpret_cast<std::array<unsigned char, sizeof(float)>&>(f)и получить точно такой же вывод. Это доказывает что-нибудь?
Evg
@LF согласно спецификации: std::arrayудовлетворяет требованиям ContiguiosContainer (начиная с C ++ 17) .
Мануэль Гил
1
@ManuelGil: std::vectorтакже удовлетворяет тем же критериям и, очевидно, не может использоваться здесь. Есть ли что-то, что требует, std::arrayчтобы элементы содержались внутри класса (в поле), не давая ему быть простым указателем на внутренний массив? (как в векторе, который также имеет размер, который массив не должен иметь в поле)
firda
@firda Совокупное требование std::arrayэффективно требует, чтобы оно содержало элементы внутри, но я беспокоюсь о проблемах макета.
LF