Russ Schultz
Russ Schultz

Reputation: 2689

How to do compile time error checking in constexpr?

In modeling hardware registers in C++11/14, I'm doing some experimentation with constexpr and a few other things. I'm having an issue getting the compiler to complain where I want it to.

In my class below, I'm defining a register as being 32 bits, and containing fields of varying width with enumerated values. The enum name is unique to the register so there will never be FIELD1_ON and FIELD2_ON. I build up the enum to be <32 bits of field mask> and <32 bits of enum value> so I can manage only setting the required bits and not changing bits I'm not interested in.

I want to be able to express multiple fields by saying ENUM1 | ENUM2 | ENUM3, but this is where I'm having trouble.

If ENUM1 and ENUM2 are in the same field I want it to give me a compilation error if possible, and then a runtime error if not.

Here's my code

constexpr uint32_t ffs ( uint32_t value )
{
    constexpr uint32_t DeBruijnSequence[32] =
    {
        0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
        31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
    };    
    return  DeBruijnSequence[ 
        (( ( value & ( -static_cast<int32_t>(value) ) ) * 0x077CB531ULL ) & 0xFFFFFFFF)
            >> 27];
}

#define MAKE_ENUM_VAL(field,value) ((static_cast<uint64_t>(field)<<32ULL) | (value<<ffs(static_cast<uint32_t>(field))))

class Register
{
public:

    enum struct Fields
        {
            FIELD1 =  0b00000000000000000000000000000001,
            FIELD2 =  0b00000000000000000000000000001110,
            FIELD3 =  0b00000000000000000000000011110000,
            FIELD4 =  0b00000000000000001111111100000000,
        };
    enum struct Enums : uint64_t 
    {
        ON    =  MAKE_ENUM_VAL(Register::Fields::FIELD1, 1),
        OFF   =  MAKE_ENUM_VAL(Register::Fields::FIELD1, 0),
        MODE0 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 1),
        MODE1 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 2),
        MODE2 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 3),
        MODE3 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 4),
        MODE4 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 5),
        MODE5 =  MAKE_ENUM_VAL(Register::Fields::FIELD2, 6),
    };
};

constexpr Register::Enums operator | (Register::Enums a, Register::Enums b)
{
    return 
        ((static_cast<uint64_t>(a) & static_cast<uint64_t>(b)) == 0) ? 
              static_cast<Register::Enums>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b))
            : throw "Bad juju";
}

Here's my test code:

    Register::Enums MyEnum1 = Register::Enums::ON | Register::Enums::OFF; //desire failed compilation
    Register::Enums MyEnum2 = Register::Enums::ON | Register::Enums::MODE1; 
    Register::Enums MyEnum3 = Register::Enums::ON;
    Register::Enums MyEnum4 = Register::Enums::OFF;
    Register::Enums MyEnum;

    MyEnum = MyEnum3 | MyEnum4; //assert/throw at runtime

I WANT the compilation of MyEnum1 to fail. (Trying to set FIELD1 to both ON and OFF). I've tried doing a check in the | operator (&ing the bits of a and b should be result in 0, meaning no overlap), but the best I can get it to do is fail over to the runtime version and then hit the assert.

I've tried fooling with static assert(), but it seems like constexpr doesn't like that inside of it. I've fiddled with some other things (trying to detect when I'm in the constexpr using some oddball constructs I've found here on stackoverflow), but I can't seem to find anything that gives me the behavior I want.

Any suggestions that don't include C++17/20? My embedded compilers aren't that advanced.

Upvotes: 1

Views: 407

Answers (1)

Sprite
Sprite

Reputation: 3763

The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time. Such variables and functions can then be used where only compile time constant expressions are allowed (provided that appropriate function arguments are given). (cppref)

constexpr does not guarantee forced compile-time evaluation, it allows parameters to be run-time values, so static_assert does not work here.

A solution is to use the non-type template parameter, which requires the value to be a constant.

But the disadvantage is that you can no longer use the "|" (or) operator, but a new function instead.

The new static member function of Register:

template <Register::Enums a, Register::Enums b>
static constexpr Register::Enums flag_or()
{
    static_assert((static_cast<uint64_t>(a) & static_cast<uint64_t>(b)) == 0);
    return static_cast<Register::Enums>(static_cast<uint64_t>(a) | static_cast<uint64_t>(b));
}

and usage:

int main()
{
    Register::Enums MyEnum1 = Register::flag_or<Register::Enums::ON, Register::Enums::OFF>(); //desire failed compilation
    Register::Enums MyEnum2 = Register::flag_or<Register::Enums::ON, Register::Enums::MODE1>(); 
    constexpr Register::Enums MyEnum3 = Register::Enums::ON;
    constexpr Register::Enums MyEnum4 = Register::Enums::OFF;
    Register::Enums MyEnum;

    MyEnum = Register::flag_or<MyEnum3, MyEnum4>(); //assert/throw at runtime
}

Output:

Compiler returned: 1
Compiler stderr
<source>: In instantiation of 'static constexpr Register::Enums Register::flag_or() [with Register::Enums a = Register::Enums::ON; Register::Enums b = Register::Enums::OFF]':
<source>:50:91:   required from here
<source>:43:77: error: static assertion failed
   43 |         static_assert((static_cast<uint64_t>(a) & static_cast<uint64_t>(b)) == 0);
      |                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~

Link to godbold.


In C++20, the consteval keyword could probably be applied to the "|" (or) operator function, but unfortunately it doesn't work for your case.

Upvotes: 1

Related Questions