bcumming
bcumming

Reputation: 1125

Changing type without changing bits

I want to take a stack variable and reinterpret cast it into an unsigned integer type of the same size in bytes. For example, I might want to take double value and cast it to an uint64_t, with the catch that the bits are not modified. And I want to do it in a generic fashion.

If I was dealing with pointers, I would use a reinterpret_cast<uint64_t*>(double_ptr).

I have come up with a solution, which uses a dirty hack on reinterpret_cast, and is effective, but it requires quite a lot of meta-programming to get a fairly simple outcome.

The question: is there a better way to do this? I am sure that there is, and that I am making this more complicated than need be.

I did think about using a templated union of type T and appropriately sized int_t, but that seemed even hackier, and seemed to play with undefined behavior.

edit I understand that the standard doesn't specify that double should be 64 bits, as pointed out in the comments. But with a generic approach, I will be able to get an unsigned integral type the same size as double, however big that is.

#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;
}

compiling the code above with gcc gives:

> 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

Upvotes: 1

Views: 1162

Answers (2)

Jonathan Mee
Jonathan Mee

Reputation: 38919

Between std::conditional_t and std::enable_if_t I believe that you can compress all your helper and int_type definitions into a self-sufficient caster function:

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);}

I've validated that this works on both gcc 4.9.2 and Visual Studio 2015, if you only have C++11 support though you can still get this into a self-sufficient caster function:

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);}

This will pick the uint* that has the same sizeof as the type you pass to it and use that.

I have an explaination of std::enable_if over here that may be helpful to you.

Obviously this is just useful on types that are 8, 16, 32, or 64-bits in size, but if you feel like expanding it to handle other stuff, just add another conditional_t!


If you are only ever going to pass in 8, 16, 32, or 64-bit types you can get away with less protection in your template:

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);}

This works for C++14, the C++11 equivalent is:

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);}

This is less forgiving than the conditional_t/enable_if_t template because of how I am indexing the std::tupple. size_t is an integral type so any type of any size less than 128-bits will cast to a valid std::tuple index. So for example a struct that was 3-bits in size would be cast to a uint16_t, while the desired result would probably have been for it to fail to compile.

Upvotes: 2

Casey
Casey

Reputation: 42554

If you don't want to have undefined behavior due to violating the aliasing restrictions (C++11 3.10/10) then you need to access the object representations as characters:

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;
}

High quality compilers will optimize the copy away. E.g., this program:

int main() {
    return caster(3.14f);
}

effectively optimizes to return 1078523331; on Intel processors.

Upvotes: 7

Related Questions