Changing type without changing bits
Asked Answered
K

2

1

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
Kermie answered 20/2, 2015 at 16:8 Comment(7)
Whether this is doable at all depends on whether you want to restrict yourself to one particular system and compiler. There is no standards-compliant cross-platform solution. It's not even guaranteed that double is as large as a uint64_t.Menides
If we ignore the conditional statements in the static_assert() statements, where assumptions about the size of float and double are made, the size of a double is not assumed to be 64 bit (though, in reality, I am happy to assume that floats and doubles are 32 and 64 bits)Kermie
can't you just cast a pointer to the bits and then dereference it. (like you'd do with a member function pointer)?Minor
Why do you want to cast it to integer type?Fearnought
It's called type punning and is usually done using unions.Turf
@Minor that is effectively what I am doing, though my castor() function actually makes a copy. I am interested in ways to do this without the pointer casting.Kermie
@JoachimPileborg using a union was my first idea. Then I found that the C++ standard explicitly states that the union member used to read from a union must be the same as the member last written to. Indeed, if I used auto to access a field in a union, it would always cast to the last written fields, not the type of the field I was accessing. Otherwise you get undefined behaviour. This example you show is for C99, which explicitly allows this technique.Kermie
T
7

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.

Timoteo answered 20/2, 2015 at 17:34 Comment(2)
Good point! Is the behaviour still undefined if both types are the same size, which is the case here, when I only intend to use this for fundamental types, not compound types (which some more template work could enforce)?Kermie
After doing some more reading, there is nothing in the standard about the size of the types.Kermie
H
2

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.

Hitherward answered 20/2, 2015 at 17:10 Comment(5)
That is a good point about std::enable_if... I had spent a couple of minutes wondering how to get rid of the helper template.Kermie
@Kermie I've improved my code a bit further, but I didn't bother changing the reinterpret_cast. Casey does have the right of it though that it is in violation of aliasing restrictions; a copy_n should be used instead: stackoverflow.com/questions/28697626/…Hitherward
I wonder if putting all of the conditionals together makes things any clearer? My final solution tested whether alignof(T)==alignof(int?_t), and chose reinterpret_cast or copy_n according to the result. github.com/bcumming/cpp-etc/blob/master/punning/pun.cppKermie
@Kermie As far as choosing I think you must use copy_n, because it is very likely that your compiler will inline this function which could easily cause aliasing errors in your code. As far as cleaning up the conditions, I've chosen to encapsulate everything in the function cause I didn't think there would be a lot of potential for reuse of something like a "templatized-case statement". But you know your code base, if that's something you could reuse, it would clean up the caster a lot.Hitherward
@Kermie OK, this is the last edit, I found a cool way to index a std::tuple over here, which could be used to simplify caster. I'm done now, thanks for the great problem!Hitherward

© 2022 - 2024 — McMap. All rights reserved.