3

I found out a planty of interesting things when I programming, and one of they is:

enum class Foo {
  FOO_THING,
  FOO_TOO
};

int main() {
  Foo foo {'d'};    // It is OK
  Foo foo2 {3};    // and that one too
}

Debugger says that its value is:

foo:  (unknown: 100)
foo2: (Foo::FOO_TOO | unknown: 2)

Could you tell me, why it is allowed to initialize Foo with values that exceed declared enum class values?

Compiler version and command:

Compiler GCC (g++ (Ubuntu 7.3.0-21ubuntu1~16.04) 7.3.0)
$ g++ -Wall -std=c++17 foo.cpp

I really want to know a reason of creating that initialization mechanism in C++17?

As far as I know enum class is created, to not allow users to use enums in that way.

BartekPL
  • 2,290
  • 1
  • 17
  • 34
  • I don't know the official reasons, but flag enums are one candidate. In addition, the compiler can't check a runtime initializer. – chris Jun 06 '18 at 11:12
  • `enum` does not restrict range of values held to those values declared inside of it. Some enums, such as `std::byte` don't even declare any known values. – user7860670 Jun 06 '18 at 11:16
  • @chris: Flag enums were indeed specifically considered by WG21. The wording was indeed written such that `eVal1 | eVal2` would also in range if both `eVal1` and `eVal2` are. – MSalters Jun 06 '18 at 11:36

3 Answers3

4

Every enum class has an underlying type, which must be at least one byte big. Any single-character constant like 'd' fits in a byte.

( C++ defines a byte as the amount of storage for one character. 8 bits are called an "octet" in C++ jargon)

Note that for "old" enums, the range is defined more restrictively, but that affects the conversion rules. You still have an underlying type, and that still has to be one byte big.

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • The last paragraph is wrong or misleading. The underlying type of non-scoped enums does not determine the range of values, and only conversion within the range is defined. [expr.static.cast] 5.2.8/10 "A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the resulting value is unspecified (and might not be in that range)." -- and the range of the enumeration values of an unscoped enum **is not the range of the underlying type** [dcl.enum] 7.2/8 – Yakk - Adam Nevraumont Jun 06 '18 at 18:15
4

Could you tell me, why it is allowed to initialize Foo with values that exceed declared enum class values?

As one of other answers says - enums are integers under the hood. It is implementation-specific which integral type is used for specific enum. That is why You can assign integer to enum.

As far as I know enum class is created, to not allow users to use enums in that way.

This is actually untrue - enum class exists to be unable to implicitly cast enum TO integer, not the other way. Notice that implicit casting of int to clas enum does not pose much possible threat.

Note that explicit casts are perfectly valid and sometimes useful.

Enums as bit flags

Sometimes enums can be used as bit flags

enum class O{
    NOTHING = 0,
    VERBOSE = 1,
    QUIET = 2,
    LOG = 4
};

Now imagine You want to pass Your options, but You want Your output to be logged and verbose. So You should pass 4 | 1 = 5. This exceeds the enum value. For me it is only valid (to some extent, there are better solutions to this problem) usage of class enum exceeding the max value.

Community
  • 1
  • 1
bartop
  • 9,971
  • 1
  • 23
  • 54
  • What is the purpose of using an enum class like integer (that exceed declared values)? – BartekPL Jun 06 '18 at 11:27
  • I do not see a valid purpose and I would strongly discourage solution like that. And purpose of existance in standard? I may only guess that maybe some compatibility with old enums wanted to be maintained – bartop Jun 06 '18 at 11:29
  • But old enums are still available in standard – BartekPL Jun 06 '18 at 11:30
  • @BartekPL, That logic doesn't always work out. Even if `enum` no longer existed, compilers still have flags to accept old C++ code, but new C++ features still try their darndest to retain backward-compatibility. This allows users to migrate to newer features and gain their benefits without needing to change everything. In the case of `enum class`, that means scoping and lack of implicit integral conversions. – chris Jun 06 '18 at 11:45
  • This example will not work. You need to define an `operator|` first to use it in that way. – BartekPL Jun 06 '18 at 11:50
  • @BartekPL The operator | was just a demonstration what I mean. I know it cannot be used on enum class. But You can explicitly pass number 5 as option. – bartop Jun 06 '18 at 11:52
  • I'd go with reasonable, not necessarily good one. I mean, I would not do it like that, it isn't really convenient and/or clear what is going on. – bartop Jun 06 '18 at 11:55
2

[dcl.enum]/8:

For an enumeration whose underlying type is fixed, the values of the enumeration are the values of the underlying type.

All enum class have a fixed underlying type; either explicit, or int if not given explicitly.

So they are guaranteed to be able to store anything their underlying type can.

Unscoped enums (enum without class or struct) have different rules; their valid values are basically the 2s complement binary cube of the enumerator values. If your enum was unscoped, setting its value to 3 wouldn't be portable.

I suspect this rule for scoped enums was done in order to make it easier to check correctness in some sense; they are guaranteed to be some integral type. Checking correctness of unscoped enum use was hard. It does mean you cannot as a programmer assume enums are only in the state of their enumerated values; but ensuring that would both ruin lots of use cases for enum and be difficult to guarantee practically.

You could probably make a proposal for a strict enum, which can only be in the named states, if you see a huge advantage from it.

For non-scoped enums: [dcl.enum] 7.2/8

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 e_min is the smallest enumerator and e_max is the largest, the values of the enumeration are the values in the range b_min to b_max , defined as follows: Let K be 1 for a two’s complement representation and 0 for a one’s complement or sign-magnitude representation. b_max is the smallest value greater than or equal to max ( | e_min |− K, | e_max | ) and equal to 2^M − 1 , where M is a non-negative integer. b_min is zero if e_min is non-negative and − ( b_max + 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 b_min 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

[expr.static.cast] 5.2.8/10:

A value of integral or enumeration type can be explicitly converted to an enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the resulting value is unspecified (and might not be in that range).

converting to a non-scoped enum outside the binary closure of the values of the enum results in an unspecified value, even if it fits in the underlying type.

enum foo{zero, one};

the underlying type of foo is going to be some integral type, and foo is layout-compatible with that integral type, and that integral type may hold 2 just file, but converting 2 to foo results in foo storing an unspecified value.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Something tells me such a "strict enum" would be a perfect candidate for a metaclass. – chris Jun 06 '18 at 11:40
  • 1
    Actually, setting the value of any enum to 3 would be portable. See the footnote on the relevant rule: "This set of values is used to define promotion and conversion semantics for the enumeration type. It does not preclude an expression of enumeration type from having a value that falls outside this range.". I.e. this range does not affect the underlying type. – MSalters Jun 06 '18 at 11:50
  • 1
    @msalt an integral type whose only valid values are 0 and 1 couldn't fit a 3, and if unscoped `enum {A,B}` is only guaranteed to be able to store 0 and 1 if I understand the closure rules in the standard. I miss something? – Yakk - Adam Nevraumont Jun 06 '18 at 11:53
  • @Yakk-AdamNevraumont: In my reading, it could fit 3 - the underlying type must have at least 8 bits. And for conversion rules, it wouldn't matter whether it behaves like it had 1,2 or 7 bits. Now the footnote does become relevant when the range was something like `[0,511]` which fell between two integral sizes. – MSalters Jun 06 '18 at 12:01
  • @MSalters Quotes added. It can *fit* 3, but you cannot *give 3 to it* because the result of giving 3 to it is an unspecified value. The assignment of 3 (or the initialization with 3) to a non-scoped enum is *not* the same as assigning 3 to the underlying type of the non-scoped enum. – Yakk - Adam Nevraumont Jun 06 '18 at 18:18