Search code examples
c++bitvariable-declaration

Is it possible to declare a type with a set bit width in c++ without using a struct?


It's been at least a decade since I last touched C++. Supposedly, C++ has had a minor overhaul. Is there a way to specify the bit width of a type without using struct, class, or union bitfields? The problem with them is that it adds an unnecessary and annoying level of indirection:

  struct Address
  {
        unsigned char val:4 = 0; // C++ in 2020?
  };

  struct Device
  {
        Address address;    // 4-bit address
        string name;
  };

  int main() 
  {
        Device device;
        device.address.val = 0x8;  // Yuckity yuck WTF!
        return 0;
  };

If C++ had properties like C#, you could make Address an accessor that hides away the indirection. In Ada, you would simply declare Address like so:

type Address is range 0..2**4 - 1 with Object_Size = 4;  -- Isn't this cute and sweet!

I tried the following declaration and there's no reason why it shouldn't work:

typedef unsigned char Address:4;   // if we were only so lucky!

Does C++ support such a construct or workaround?


Solution

  • Not out of the box, but it is not so hard to make a small wrapper class that does the validation. Sketch of such a class here, it can even check at compile time.

    Demo here : https://onlinegdb.com/RBVQfsAI5

    #include <iostream>
    
    // N is the number of address bits you want to use
    template<std::size_t N>
    struct address_t
    {
    public:
        explicit constexpr address_t(std::uintptr_t value) :
            address{ checked_value(value) }
        {
        }
    
        // one of the rare cases implicit conversion is useful
        operator std::uintptr_t() const noexcept
        {
            return address;
        }
    
        // assigment operator checks the value.
        void operator=(std::uintptr_t value)
        {
            address = checked_value(value);
        }
    
    private:
        constexpr std::uintptr_t checked_value(std::uintptr_t value)
        {
            if (value >= (1 << N)) throw std::invalid_argument("address value too large");
            return value;
        }
    
        std::uintptr_t address;
    };
    
    int main()
    {
        //addres_t<4> means an address with max. 4 bits
        static constexpr address_t<4> compile_time_address{15}; // will not compile if value too big
        std::cout << compile_time_address << "\n"; // uses implicit conversion to std::uint_ptr_t
    
        address_t<4> runtime_address{15};
        std::cout << runtime_address << "\n";
        runtime_address = 12;
        std::cout << runtime_address << "\n";
    
        try
        {
            // at runtime assigning an invalid value will throw.
            address_t<4> runtime_address_fail{16};
        }
        catch (const std::invalid_argument&)
        {
        }
    
        return 0;
    }