How can I convert ANY user defined type to an std::bitset?

443 Views Asked by At

What I want to achieve is a means of converting any arbitrarily sized and formatted type to an std::bitset. Like this:

 #include<bitset>
 #include<bit>
 #include<cstdlib>
 #include<cstdint>
 #include<array>
 #include<iostream>

 template<typename T, std::size_t SIZE = (sizeof(T) * CHAR_BIT)>
 std::bitset<SIZE> as_bits(const T var) noexcept
 {
    if constexpr (SIZE < 32)//Size in bits
    {
        int32_t temp = 0;
        std::memmove(&temp, &var, sizeof(T));

        std::bitset<SIZE> bits = var;
        return bits;
    }//End if
    else
    {
        std::bitset<SIZE> bits = std::bit_cast<std::bitset<SIZE>, T>(var);
        return bits;
    }//End else
 }//End of as_bits

Usage:

 float x = 4.5f;
 std::cout << x << " as bits: " << as_bits(x) << "\n";

 #pragma pack(push)
 struct Y
 {
     std::array<int32_t, 4> z;
     float x;
     int8_t y;
 };
 #pragma pack(pop)
 Y y = { {1,2,3,4}, 3.5, 'a'};

 std::cout << "struct as bits: " << as_bits(y) << "\n";
 std::cout << "size of bitset: " << as_bits(y).size() << " bits long.\n";

Output:

 4.5 as bits: 01000000100100000000000000000000
 struct as bits: 000000000000000000000000011000010100000001100000000000000000000000000000000000000000000000000100000000000000000000000000000000110000000000000000000000000000001000000000000000000000000000000001
 size of bitset: 192 bits long.
 

This works for correctly the float but the struct when converted outputs 192 bits when it should only be 168 bits in size. What's going on I've got #pragma pack?

  • How can I prevent padding? Should I even?
  • Is there a way to lockout padded types using concepts or type traits?
  • Is this undefined behavior?
  • Does endian-ness matter?
  • Is there a better way?

I'm using MSVC at the moment but a cross-platform implementation would be ideal.

On MSVC changing #pragma pack(push) to #pragma pack(push, 1) results in the following error: Error C2783 '_To std::bit_cast(const _From &) noexcept': could not deduce template argument for '__formal'

Does bit_cast require default padding and alignment?

Updated with a work around for types less than 32-bits in width.

1

There are 1 best solutions below

0
Nicol Bolas On

What you want is not generally possible. Any user-defined type which is not trivially copyable is immediately off the table, because bit_cast only works on trivially copyable types.

Speaking of which, bitset itself is not required by the standard to be trivially copyable. I mean, there's pretty much no reason why an implementation of it wouldn't be, but there is nothing in the standard which requires implementers to make it trivially copyable. So while your code may function on some particular implementation (or likely all of them), there is no guarantee that you can do a bit_cast to a bitset at all.

As for why it can break with padding, this is likely because bit_cast also requires the two types to be the same size, and bitset<N> is not required to be N/8 bytes in size. Many implementations of bitset store the bits in arrays of 32-bit integer types. So a bitset<24> may still take up 4 bytes of storage. If you were given a 3-byte type, then you can't bit_cast them.

Odds are good that what you really want is an std::array<std::byte, sizeof(T)>. While this type is trivially copyable (so bit_cast can work on it), there actually isn't a requirement that the size of such an array is equal to the sizeof(T). It usually will be, but you can't guarantee it. The size will be implementation-dependent, so whether bit_casting from a trivially copyable T works will be implementation-dependent.

What's going on I've got #pragma pack?

#pragma pack can't break the rules of C++. And there are two rules of C++ that are important here:

  1. sizeof(T) is also the number of bytes from one T to another T in an array of T.

  2. Every T must be aligned to its alignof(T) alignment. Even if the T is an element in an array.

pack can't break these rules. Since your array and float are both undoubtedly aligned to 4 bytes, T must also be aligned to 4 bytes. And since a 21-byte array increment would not reach the 4 byte alignment needed by T, the size of T must be padded out to 24.

#pragma pack only plays around with packing within the rules of C++'s requirements.