Изменение типа без изменения битов
Я хочу взять переменную стека и reinterpret cast
в беззнаковый целочисленный тип того же размера в байтах. Например, я мог бы взять значение double
и передать его в uint64_t
с уловкой, что биты не будут изменены. И я хочу сделать это в общем виде.
Если бы я имел дело с указателями, я использовал бы reinterpret_cast<uint64_t*>(double_ptr)
.
Я придумал решение, которое использует грязный взломать reinterpret_cast
и эффективен, но для получения довольно простого результата требуется довольно много метапрограмм.
Вопрос: есть ли лучший способ сделать это? Я уверен, что есть, и что я делаю это более сложным, чем нужно.
Я подумал об использовании шаблонного объединения типа T
и соответствующего размера int_t
, но это казалось даже хакером и, казалось, играло с поведением undefined.
edit Я понимаю, что стандарт не указывает, что double должен быть 64 бит, как указано в комментариях. Но с общим подходом я смогу получить целочисленный тип без знака того же размера, что и двойной, но большой.
#include <iostream>
template <typename T, std::size_t S>
struct helper {};
template <typename T>
struct helper<T, 1> {
using type = uint8_t;
};
template <typename T>
struct helper<T, 2> {
using type = uint16_t;
};
template <typename T>
struct helper<T, 4> {
using type = uint32_t;
};
template <typename T>
struct helper<T, 8> {
using type = uint64_t;
};
template <typename T>
using int_type = typename helper<T, sizeof(T)>::type;
template <typename T>
int_type<T> caster(T value) {
int_type<T> v;
*reinterpret_cast<T*>(&v) = value;
return v;
}
int main(void) {
{
auto val = caster(0.);
static_assert(std::is_same<uint64_t, decltype(val)>::value, "no good");
std::cout << sizeof(val)*8 << " " << val << std::endl;
}
{
auto val = caster(0.f);
static_assert(std::is_same<uint32_t, decltype(val)>::value, "no good");
std::cout << sizeof(val)*8 << " " << val << std::endl;
}
{
auto val = caster(-0.);
static_assert(std::is_same<uint64_t, decltype(val)>::value, "no good");
std::cout << sizeof(val)*8 << " " << val << std::endl;
}
{
auto val = caster(-0.f);
static_assert(std::is_same<uint32_t, decltype(val)>::value, "no good");
std::cout << sizeof(val)*8 << " " << val << std::endl;
}
return 0;
}
компиляция кода выше с помощью gcc дает:
> g++ --version
g++ (GCC) 4.8.2 20131016 (Cray Inc.)
> g++ -std=c++11 test.cpp && ./a.out
64 0
32 0
64 9223372036854775808
32 2147483648
Ответы
Ответ 1
Если вы не хотите иметь поведение undefined из-за нарушения ограничений псевдонимов (С++ 11 3.10/10), вам необходимо получить доступ к представлениям объектов в виде символов:
template <typename T>
int_type<T> caster(const T& value) {
int_type<T> v;
static_assert(sizeof(value) == sizeof(v), "");
std::copy_n(reinterpret_cast<const char*>(&value),
sizeof(T),
reinterpret_cast<char*>(&v));
return v;
}
Высококачественные компиляторы оптимизируют копию. Например, эта программа:
int main() {
return caster(3.14f);
}
эффективно оптимизируется до return 1078523331;
на процессорах Intel.
Ответ 2
Между std::conditional_t
и std::enable_if_t
Я считаю, что вы можете сжать все ваши определения helper
и int_type
в самостоятельную функцию caster
:
template <typename T>
auto caster(T value){return reinterpret_cast<std::conditional_t<sizeof(T) == sizeof(uint8_t),
uint8_t,
conditional_t<sizeof(T) == sizeof(uint16_t),
uint16_t,
conditional_t<sizeof(T) == sizeof(uint32_t),
uint32_t,
enable_if_t<sizeof(T) == sizeof(uint64_t),
uint64_t>>>>&>(value);}
Я подтвердил, что это работает как на gcc 4.9.2, так и на Visual Studio 2015, если у вас есть поддержка только С++ 11, хотя вы все равно можете получить это в самодостаточной функции caster
:
template <typename T>
typename std::conditional<sizeof(T) == sizeof(uint8_t),
uint8_t,
typename conditional<sizeof(T) == sizeof(uint16_t),
uint16_t,
typename conditional<sizeof(T) == sizeof(uint32_t),
uint32_t,
typename enable_if<sizeof(T) == sizeof(uint64_t),
uint64_t>::type>::type>::type>::type caster(T value){return reinterpret_cast<decltype(caster(value))&>(value);}
Это выберет uint*
, который имеет тот же sizeof
, что и тип, который вы передаете ему, и используйте его.
У меня есть объяснение std::enable_if
над здесь, которое может быть вам полезно.
Очевидно, что это просто полезно для типов размером 8, 16, 32 или 64 бит, но если вы хотите расширить его для обработки других вещей, просто добавьте еще один conditional_t
!
Если вы когда-либо будете проходить в 8, 16, 32 или 64-битных типах, вы можете уйти с меньшей защитой в своем шаблоне:
template <typename T>
auto caster(T value){return reinterpret_cast<std::tuple_element_t<size_t(log2(sizeof(T))), std::tuple<uint8_t,
uint16_t,
uint32_t,
uint64_t>>&>(value);}
Это работает для С++ 14, эквивалент С++ 11:
template <typename T>
typename std::tuple_element<size_t(log2(sizeof(T))), std::tuple<uint8_t,
uint16_t,
uint32_t,
uint64_t>>::type caster(T value){return reinterpret_cast<decltype(caster(value))&>(value);}
Это менее прощает, чем шаблон conditional_t
/enable_if_t
из-за того, как я индексирую std::tupple
. size_t
является интегральным типом, поэтому любой тип любого размера, меньшего 128 бит, будет присвоен действительному индексу std::tuple
. Так, например, struct
, размер которого был 3-битным, был бы отличен до uint16_t
, тогда как желаемый результат, вероятно, был бы для того, чтобы он не смог скомпилировать.