0

I would like to model a status register in C/C++, which should be accessible as std::bitset and as std::uint8_t. Thus I would combine them as union as follows:

#include <bitset>
#include <iostream>

union byte {
        std::uint8_t uint;
        std::bitset<8> bitset;
};

int main(int, char*[])
{
        byte b = { .uint = 0b10101010 };

        std::cout << "Value of bit 5: "
                << (b.bitset.test(5) ? "true" : "false") << std::endl;

        std::cout << "Value of bit 4: "
                << (b.bitset.test(4) ? "true" : "false") << std::endl;

        std::cout << "Bitset Output: " << b.bitset << std::endl;
        std::cout << "Uint Output: " << static_cast<int>(b.uint) << std::endl;

        return 0;
}

This seems to work as expected, when compiled with GCC x86_64 8.2. However, I would like to know if I could expect this to work in all cases or if I am better off with some helper functions like bitset, bittest, ...

timrau
  • 22,578
  • 4
  • 51
  • 64
Phidelux
  • 2,043
  • 1
  • 32
  • 50
  • 4
    Type punning is undefined behavior, so you can't expect it to work. Note, that it seems to be working by pure chance (as is usually the case with UB) since std::uint8_t and std::bitset<8> have different representations in memory (one is 1 byte and the other is 8 bytes). You can just use bitset with to_ulong method (and cast that to `uint8_t`), to avoid UB. – Dan M. Sep 10 '18 at 11:55
  • Type-punning using unions is allowed in C, but not in C++. I also don't think there's any requirement that the data layout of a `std::bitset` have to be directly compatible with a built-in type corresponding to the bit-size. All in all, attempting to do what you do will lead to [*undefined behavior*](https://en.wikipedia.org/wiki/Undefined_behavior) – Some programmer dude Sep 10 '18 at 11:59
  • @DanM. Partially wrong, the template argument for `std::bitset` is the number of *bits* not bytes. – Some programmer dude Sep 10 '18 at 12:00
  • As for your problem, if you want a type that can act as both a native `uint8_t` and handle bits in a "nice" way, then you have to implement such a class yourself. If you need it to map to a memory-mapped hardware register it should probably wrap a *pointer* to the register. – Some programmer dude Sep 10 '18 at 12:02
  • @Someprogrammerdude and? `bitset` is not guaranteed to take exactly `N/8` bytes. See https://godbolt.org/z/oBRgT9 There is nothing wrong with my statement. – Dan M. Sep 10 '18 at 12:02
  • @DanM. True, but that's because the internal layout of `std::bitset` is *implementation specific*. It doesn't have to be a single byte, or a set of single bytes. It can quite literally be *anything* which is what makes the punning even worse as the types are probably not compatible in any way or form. – Some programmer dude Sep 10 '18 at 12:04
  • @Someprogrammerdude Yup. I was referring to the provided "seems to work" godbolt link. I.e. that even in such case it works purely by chance and can't be relied upon even for this specific compiler. – Dan M. Sep 10 '18 at 12:08
  • Thank you all, @DanM. make it an answer and I will accept it. – Phidelux Sep 10 '18 at 12:14
  • std::bitset<8>(std::uint8_t) – raz Mar 21 '22 at 15:35

2 Answers2

4

What you are trying to do here with union is called type punning and is Undefined Behavior in C++ (you can read more about it in this SO answer), so it's not guaranteed to work properly even on the same compiler.

Furthermore, even if it was allowed, std::bitset<8> is not guaranteed to have the same representation as std::uint8_t (and in fact it does not on any major compiler).

In your case, you could just use the regular std::bitset<8> with to_ulong method.

Another alternative is to have a wrapper class with bitset member that would provide convenience methods to assign/convert to uint8_t.

Also, if you only want some limited API of std::bitset<8>, it might be good idea (if you want to keep the size of your class as 1 byte) to wrap around std::uint8_t and implement those few methods (like test) manually.

Dan M.
  • 3,818
  • 1
  • 23
  • 41
2

I took the idea of SomeProgrammerDude's comment

As for your problem, if you want a type that can act as both a native uint8_t and handle bits in a "nice" way, then you have to implement such a class yourself. If you need it to map to a memory-mapped hardware register it should probably wrap a pointer to the register.

and tried to make it in C++. This is the sample I came up with:

#include <cassert>
#include <iostream>
#include <iomanip>

class ByteReg {
  private:
    volatile uint8_t &reg;
  public:
    explicit ByteReg(volatile uint8_t &reg): reg(reg) { }
    ByteReg(const ByteReg&) = delete;
    ByteReg operator=(const ByteReg&) = delete;
    ~ByteReg() = default;

    operator uint8_t() { return reg; }
    bool test(int i) const
    {
      assert(i >= 0 && i < 8);
      return ((reg >> i) & 1) != 0;
    }
};

int main() 
{
  volatile uint8_t hwReg = 0xaa; // 0x10101010
  ByteReg reg(hwReg);
  unsigned value = reg;
  std::cout << "reg: 0x" << std::hex << std::setw(2) << std::setfill('0')
    << value << '\n';
  for (int i = 0; i < 8; ++i) {
    std::cout << "bit " << i << ": "
      << (reg.test(i) ? "set" : "unset") << '\n';
  }
  return 0; 
}

Output:

reg: 0xaa
bit 0: unset
bit 1: set
bit 2: unset
bit 3: set
bit 4: unset
bit 5: set
bit 6: unset
bit 7: set

Live Demo on coliru

Though, a free-standing function testBit() might do as well with even less code:

#include <cassert>
#include <iostream>
#include <iomanip>

bool testBit(uint8_t reg, int i)
{
  assert(i >= 0 && i < 8);
  return ((reg >> i) & 1) != 0;
}

int main() 
{
  volatile uint8_t reg = 0xaa; // 0x10101010
  unsigned value = reg;
  std::cout << "reg: 0x" << std::hex << std::setw(2) << std::setfill('0')
    << value << '\n';
  for (int i = 0; i < 8; ++i) {
    std::cout << "bit " << i << ": "
      << (testBit(reg, i) ? "set" : "unset") << '\n';
  }
  return 0; 
}

Live Demo on coliru

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56