Given enum class val { foo = 1, bar = 2, baz = 4 };
It is possible to define:
val operator|(val x, val y)
{
return static_cast<val>(static_cast<int>(x) | static_cast<int>(y));
}
However, is it semantically correct to do so?
I am leaning towards no, as demonstrated in the following, seemingly well-behaving example:
int convert(val x)
{
switch(x)
{
case val::foo: return 42;
case val::bar: return 53;
case val::baz: return 64;
}
}
Calling convert(val::foo | val::bar)
will return 0
when compiled with g++ and segmentation fault with clang++.
Here is g++ version. And here is clang++ version.
My question is two-fold:
1.a Which compiler is correct in the above linked examples, g++ or clang++?
I can think of several possible implementations:
enum class val { foo, bar, baz, size };
using val_flags = std::set<val>; // (1)
using val_flags = std::vector<bool>; // (2)
using val_flags = std::bitset<val::size>; // (3)
using val_flags = std::underlying_type<val>::type; // (4)
UPDATE:
Thank you all for your answers. I ended up resurrecting my old enum operator template. In case anybody is interested, it can be found here: github.com
Enumerations in C++ come in two forms: scoped enumerations in which the underlying type is fixed and unscoped enumerations in which the underlying type may or may not be fixed.
Yes. In C enum types are just int s under the covers. Typecast them to whatever you want. enums are not always ints in C.
You can assign different values to enum member. A change in the default value of an enum member will automatically assign incremental values to the other members sequentially.
C++ Explicit type conversions Enum conversions static_cast can convert from an integer or floating point type to an enumeration type (whether scoped or unscoped), and vice versa. It can also convert between enumeration types.
following, seemingly well-behaving example:
It's not, but make one minor change:
int convert(val x)
{
switch(x)
{
case val::foo: return 42;
case val::bar: return 53;
case val::baz: return 64;
}
return 9; // ADDED THIS LINE
}
and all will be well. An alternate fix would be to use a default:
case and return there.
Your existing code triggers undefined behavior1 by reaching the closing brace of a function with a non-void
return type. Because it is undefined behavior, both compilers are correct.
The semantics of holding values in an enum
type which are bitwise OR combinations of enumerator values are well-defined and guaranteed. The standard requires that instances of the enum
can store any integer value with no more bits used than any of the enumerator values defined, which includes all bitwise-OR combinations. The formal language used to say this is a bit messy, but here it is (note that your case is an enum class
, these always have fixed underlying type and the first sentence applies):
For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type. Otherwise, for an enumeration where emin is the smallest enumerator and emax is the largest, the values of the enumeration are the values in the range bmin to bmax, defined as follows: Let K be 1 for a two’s complement representation and 0 for a ones’ complement or sign-magnitude representation. bmax is the smallest value greater than or equal to max(|emin| − K, |emin|) and equal to 2M − 1, where M is a non-negative integer. bmin is zero if emin is non-negative and −(bmax + K) otherwise. The size of the smallest bit-field large enough to hold all the values of the enumeration type is max(M, 1) if bmin is zero and M + 1 otherwise. It is possible to define an enumeration that has values not defined by any of its enumerators. If the enumerator-list is empty, the values of the enumeration are as if the enumeration had a single enumerator with value 0.
(from n4582, section 7.2 [dcl.enum]
)
1 From 6.6.3 [stmt.return]
:
Flowing off the end of a constructor, a destructor, or a function with a cv
void
return type is equivalent to a return with no operand. Otherwise, flowing off the end of a function other than main (3.6.1) results in undefined behavior.
Is it semantically correct to store values in an enum that are not represented by an enumerator? Excerpts from the standard are most welcome.
Yes if the value is in the range of the enumeration. Ben Voigt provided an excerpt from the standard. I prefer looking at cppreference because I find it more readable (although it doesn't have the same authoritative value).
Values of integer, floating-point, and other enumeration types can be converted, such as by static_cast, to any enumeration type. The result is unspecified (until C++17)undefined behavior (since C++17) if the value, converted to the enumeration's underlying type, is out of this enumeration's range. If the underlying type is fixed, the range is the range of the underlying type. If the underlying type is not fixed, the range is all values possible for the smallest bit field large enough to hold all enumerators of the target enumeration. Note that the value after such conversion may not necessarily equal any of the named enumerators defined for the type.
Which compiler is correct in the above linked examples, g++ or clang++?
The problem is that your code invokes undefined behavior, but for a reason unrelated to enumerations, as pointed out in the comments and by Ben Voigt. So both compilers are correct.
Note that you don't really need the convert
function to experiment with these behaviors.
enum class val { foo = 1, bar = 2, baz = 4 };
val operator|(val x, val y) {
return static_cast<val>(static_cast<int>(x) | static_cast<int>(y));
}
int main() {
std::cout << static_cast<int>(val::foo | val::bar); // prints 3
}
Live example
Is there a standard (or proposed) way to represent flags in C++?
I would go for static constexpr variables in a structure (or class) for the scope.
struct Flags {
static constexpr unsigned int foo = 0x01;
static constexpr unsigned int bar = 0x02;
static constexpr unsigned int baz = 0x04;
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With