1

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.

dave_thenerd
  • 448
  • 3
  • 10
  • You should specify your implementation since `#pragma pack` is a non-standard extension. – Daniel Langr Nov 25 '21 at 07:38
  • Your program doesn't compile for me: https://godbolt.org/z/G31vW1dTq (also please leave the header files in, so we don't need to spend time adding them on our own) – infinitezero Nov 25 '21 at 07:42
  • BTW, shouldn't you specify new packing alignment? Somthing as `#pragma pack(push,1)`? In your case, only current alignment is pushed, but a new one is not set. You can verify it by printing `sizeof(Y)`. Live demo: https://godbolt.org/z/8KEW44hsv. – Daniel Langr Nov 25 '21 at 07:44
  • `std::bit_cast...` requires that `sizeof(From)` equals `sizeof(To)`. When you pack `Y` with alignement requirement 1, then `sizeof(Y)` is 21, but `sizeof` the corresponding `std::bitset` may be padded. In my case, it was 24. Live demo: https://godbolt.org/z/87z7bz7bb. `std::bit_cast` then cannot be used. – Daniel Langr Nov 25 '21 at 08:02
  • 5
    Moreover, `std::bit_cast` requires both types to be _trivially-copyable_, which doesn't seem to be guaranteed for `std::bit_set`. – Daniel Langr Nov 25 '21 at 08:09
  • Hmm. Well this is problematic. I've also just realized that while this works fine for 32 and 64 bit types like int32_t, float, int64_t and double. It fails with all 8-bit and 16-bit types. – dave_thenerd Nov 25 '21 at 08:14
  • Relevant question: [Why is std::bitset<8> 4 bytes big?](https://stackoverflow.com/q/7511355/580083) I guess then that the only portable way (for trivially-copyable types) is to transform the bit-representation of the input object into an `std::string` and then creating a `std::bit_set` from this string. – Daniel Langr Nov 25 '21 at 08:16
  • Man, I wish they exposed a .data() function or allowed you to write at an index with operator[] then I could solve this with memmove() – dave_thenerd Nov 25 '21 at 08:22
  • @dave_thenerd you can of course write that function yourself. – infinitezero Nov 25 '21 at 09:17
  • 2
    Why `std::bitset`? This class is for performing logical operations on a bunch of bits. If you want to binary-serialize your data, `std::array` is a better choice. – n. m. could be an AI Nov 25 '21 at 09:58
  • @infinitezero How would you add a member function to `std::bit_set`? Do you mean writing a proposal that would introduce such a function? – Daniel Langr Nov 25 '21 at 10:45
  • No, write the functionality yourself for your code. – infinitezero Nov 25 '21 at 12:13

1 Answers1

0

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982