Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clang rejects inexplicit enum value in constexpr function (gcc works)

In the following example Clang fails only in the constexpr version of the function:

enum foo { bar };

auto baz() { return static_cast<foo>(-1); }
constexpr auto barf() { return static_cast<foo>(-1); }

Link: https://godbolt.org/z/b13M4v4xx

gcc accepts both versions.

Is this is bug in clang? If not, then why does it only fail in constexpr context?

UPDATE

If I add storage class (eg, enum foo : char { bar };), that makes clang happy... but doesn't answer my question. :)

UPDATE

If clang is correct, as several people have suggested, then why does the following fail:

enum foo { bar };

constexpr struct { int qux :1; } quux{-1};

auto baz() { return static_cast<foo>(quux.qux); }
constexpr auto barf() { return static_cast<foo>(quux.qux); }

Link: https://godbolt.org/z/61dWobbvo

According to c++17 foo should have a storage of 1-bit bit-field, which is what quux.qux is. Yet, this still fails on clang.

like image 837
Innocent Bystander Avatar asked Dec 10 '25 23:12

Innocent Bystander


2 Answers

foo is an enumeration type that is said to be without a fixed underlying type, because it isn't a scoped enumeration and doesn't explicitly specify an underlying type.

In contrast to all other enumeration types, enumerations without fixed underlying type do not share their value range with that of their underlying type.

Instead [dcl.enum]/8 specifies that their range is limited to effectively the minimal range necessary to represent all its enumerators and all of their bitwise-or combinations (at least as long as all enumerators have non-negative value).

In particular, your enumeration foo has only one enumerator with value 0. And so the range according to the above heuristic is just the value 0. The exact wording in C++17 gives the same result. In C++20 the wording was changed and it is now less clear, but that is likely just an unintended wording defect, see CWG 2932.

So, -1 is not a valid value for foo. [expr.static.cast]/10 states that behavior is undefined when attempting to cast an integral type expression to an enumeration without fixed underlying type if its value is outside the enumerations range. When undefined behavior would happen during evaluation of an expression, then that expression can't be a constant expression and therefore something like

constexpr auto r = barf();

would be ill-formed.

Now, you do not actually call barf in a context that requires a constant expression in your question. In that case, before C++23 the function definition itself was IFNDR (ill-formed, no diagnostic required) because it could never be called in a constant expression, meaning that a compiler could diagnose that constexpr on it would never actually allow it to be used in a constant expression and could fail to compile it.

With C++23 this was changed and a function declared with constexpr is now not IFNDR just because it can't be called in a constant expression. Only actually calling it where a constant expression is required is now ill-formed.

In your second example nothing changes. The value of quux.qux is still -1 and its type still int, causing the same rules to apply as above.


From my testing, at the moment Clang seems to be the only compiler that actually diagnoses use of invalid values in enumeration types during constant evaluation. The other compilers are behaving non-conforming.

Note that, although the cast is undefined behavior, as far as I am aware, in default configurations, current compilers do not actually assume that the range of enumerations without fixed underlying type is limited in the way the standard specifies. So it is relatively safe to still use it at runtime. I am at least pretty sure about GCC and Clang, both of which have a special optimization flag -fstrict-enums, which is not enabled by any -O flag, to tell the compiler to actually make use of the limited range for optimization.

like image 119
user17732522 Avatar answered Dec 13 '25 15:12

user17732522


Since C++17 an enum has defined storage as the smallest bitfield that can hold all of the enum values. So for foo that would be a bitfield of size 1. Your -1 needs ALL the bits of the underlying type and casting it is thus UB

So clang is right to reject it. This demo shows that :

enum foo { zero, one, two};

// these do compile
constexpr auto bar0() { return static_cast<foo>(1); }
constexpr auto bar1() { return static_cast<foo>(1); }
constexpr auto bar2() { return static_cast<foo>(2); }
constexpr auto bar3() { return static_cast<foo>(3); } // even though 3 is not a "valid" enum value it can be cast (to a bitfield with 2 bits)

// does not compile
constexpr auto bar4() { return static_cast<foo>(4); } // 4 doesn't fit into a 2 bit bitfield.

Also see : Lightning Talk: So You Thought C++ Was Weird? Meet Enums - Roth Michaels - CppCon 2021

like image 31
Pepijn Kramer Avatar answered Dec 13 '25 16:12

Pepijn Kramer



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!