Приведенная ниже функция шаблона является частью генератора последовательности. Вместо ручных сдвигов я придумал следующее решение на основе объединения, чтобы сделать операции более явными. Он отлично работает на всех протестированных компиляторах. Ссылка на Godbolt.
Однако, несмотря на то, что он работает на практике, я боюсь, что существуют правила алиасинга, которые нарушаются, что означает, что он может не работать в будущем или в другом компиляторе, отличном от GCC и CLANG.
Строго в соответствии со стандартом С++: правильно ли сформирован приведенный ниже код? Это связано с неопределенным поведением?
template <int BITS>
uint64_t flog2(uint64_t num) {
constexpr uint64_t MAXNUM = (uint64_t(1) << BITS);
if (num < MAXNUM) return num;
union FP {
double dbl;
struct {
uint64_t man: 52;
uint32_t exp: 11;
uint32_t sign: 1;
};
struct {
uint64_t xman: 52-BITS;
uint32_t xexp: 11+BITS;
uint32_t xsgn: 1;
};
};
FP fp;
fp.dbl = num;
fp.exp -= 1023-1+BITS;
return fp.xexp;
}
Спасибо!
или в другом компиляторе, отличном от GCC и CLANG" Известно, что компилятор Microsoft размещает битовые поля в другом порядке. «Определяется реализацией» и все такое.
Во-первых, программа синтаксически некорректна в стандарте ISO C++. Анонимные члены struct
не являются стандартными C++ (в отличие от C). Они являются расширением. В стандарте ISO C++ struct
должен быть назван и доступен через это имя.
Я проигнорирую это до конца ответа и притворюсь, что вы обращались через такое имя.
Технически это не нарушение псевдонимов, а неопределенное поведение для чтения неактивного члена объекта объединения в
fp.exp -= 1023-1+BITS;
Типы для этого не имеют большого значения (в отличие от алиасинга). Всегда существует не более одного активного члена союза, который будет последним, который был либо явно создан, либо записан с помощью выражения доступа/назначения члена. В вашем случае fp.dbl = num;
означает, что dbl
является активным участником и единственным, с которого можно читать.
В стандарте есть одно исключение для доступа к общей начальной последовательности членов стандартного типа класса макета объединения, и в этом случае доступ к неактивному можно получить, как если бы он был активным. Но даже у ваших двух членов struct {
есть непустая общая начальная последовательность только для BITS == 0
.
Однако на практике компиляторы обычно явно поддерживают такой тип каламбура, вероятно, уже для совместимости с C, где это разрешено.
Конечно, даже если оставить все это в стороне, расположение битовых полей и представления вовлеченных типов полностью определяются реализацией, и вы не можете ожидать, что это будет в целом переносимым.
Если расположение битовых полей не соблюдается, что происходит с миллионами строк C++, написанных для обработки сетевых пакетов? Скажем, структуры iphdr
или ethdr
, например?
@HenriqueBucher Дело не в уважении. Битовое поле uint64_t man: 52;
означает только то, что к man
можно получить доступ как к члену типа uint64_t
, который может хранить в нем до 52-битных целых значений. На самом деле это ничего не говорит о структуре памяти. Это добавлено реализацией. Например, MSVC иногда добавляет заполнение там, где GCC/Clang этого не делает. Вы не можете наивно использовать код, написанный для одного, с другим, полагаясь на макет битового поля, хотя, полагаясь на него, вы, вероятно, имеете UB, как я все равно описал, и это еще больше зависит от реализации (например, endianess).
Итак, вы говорите, что нет кросс-платформенного способа определить в C++ схему памяти для структуры? Это звучит как огромное ограничение для меня.
@HenriqueBucher Макет члена не может быть форсирован в стандартном C++, но можно вкладывать подобъекты в любом месте внутри члена массива unsigned char
/std::byte
и предоставлять методы доступа. Но у вас все еще есть размеры, требования к выравниванию и битовое представление типов, которые вы там храните, и это также в значительной степени определяется реализацией. Я не думаю, что стандарт ISO когда-либо предназначался для определения поведения на этом низком уровне переносимым образом. Это необходимо только для интерфейсов, специфичных для платформы, или для оптимизации, которая в любом случае также будет зависеть от реализации.
Я имею в виду, что стандарт ISO определил модели доступа к памяти, не так ли? Почему в C это есть, а в C++ нет? Я думаю, что это довольно важный вопрос, чтобы небрежно оставить его в UB.
Вы случайно не знаете, где в стандарте эти правила?
@HenriqueBucher Что касается битовых полей, некоторые детали того, что не указано / определяется реализацией, различаются, но в основном то же самое верно как для C, так и для . В C разрешена каламбуризация типов union, но результирующие значения не указаны. Модель памяти/объекта C++ описана в basic.memobj . Вам нужно взять его вместе с отдельными разделами в expr для того, кто относится к тому или иному выражению. Для битовых полей смотрите также class.bit. К сожалению, это не просто читать.
@HenriqueBucher Ну, и, конечно же, class.union за поведение профсоюзов.
@HenriqueBucher Кроме того, извините, я не имел в виду «неопределенное» выше для каламбура типа C. Я имел в виду, что результирующие значения в любом случае по-прежнему определяются реализацией, потому что представление основных типов и структура памяти структур также определяются реализацией. Однако результирующее значение может быть представлением ловушки. (Это не указано только при чтении из заполнения.)
Чтение из члена союза, который не был записан последним, является неопределенным поведением.
Кроме того, расположение битовых полей определяется реализацией.
Следовательно, с точки зрения строгого стандарта C++, этот код вызывает как неопределенное поведение (путем чтения exp
после записи dbl
), так и полагается на поведение, определяемое реализацией, в предположении, что макет битового поля соответствует представлению double
с плавающей запятой (которое, кстати, также определяется реализацией).
Интересный. Разве это не противоречит всей цели бифилдов?
запись одного значения объединения и чтение другого — это UB.