6 ms·
enum class Handle : uint32_t { Invalid = 0 }; Handle h { 42 }; // OK One of their examples demonstrates the number one issue for me with enums, which was
by lpribis 2y ago
enum class Handle : uint32_t { Invalid = 0 };
Handle h { 42 }; // OK
One of their examples demonstrates the number one issue for me with enums, which was not fixed with `enum class`. Since values outside the range of the type are valid, you are constantly needing to check for invalid values in any function that takes an enum [class]. Ruins any attempt at "parse, don't validate" style in c++ and completely ruins the "type safety" which c++ people are always going on about.
- Sharlin 2y agoWell, apparently it was fixed with `enum class`… until it was unfixed in C++17 for unfathomable reasons. It’s honestly crazy that C++ doesn’t have simple exhaustiveness-checked enums. The obvious actual solution for the valid use case of deserializing an enum from an integer would be something like template<E> std::optional<E> from_underlying(std::underlying_type<E>) requires(std::is_enum_v<E>)
- bluGill 2y agoan event loop often wants an event type enum that has defined values for internal events and then everything out of range means pass onto the users handler. There are other variations where you need to pass an enum value without caring what it mean.
- Nullabillity 2y agoThis isn't rocket science once you have sum types. enum Event<T> { System(SystemEvent), User(T), }
- account42 2y agoUsing Rusts' retarded enum keyword for your example doesn't exactly make your point understandable, especially when talking in the context of actual enumerations. Anyway, the point of what gp describes is to do this in a single integer without overhead so a naive sum type is not the solution.
- Nullabillity 2y ago> Using Rusts' retarded enum keyword for your example doesn't exactly make your point understandable, especially when talking in the context of actual enumerations. data Event = System SystemEvent | User a > Anyway, the point of what gp describes is to do this in a single integer without overhead so a naive sum type is not the solution. Virtually all event systems end up carrying auxilary event data of some form, so sum types are what you want regardless.
- bluGill 2y agoAt least in C++ a template needs to be known at compile time, but I want to build my event loop and latter add in more values that should be handled without rebuilding it.
- Nullabillity 2y agoYou could make your `T` type as dynamic as you want it to be.
- Thorrez 2y agoIt wasn't unfixed in C++17. The problem existed before C++17. This is valid code in C++11: enum class Foo : int { Invalid = 0 }; Foo f = Foo(5); In C++11, this compiles with Foo(5) but not Foo{5}. In C++17, this compiles with both Foo(5) and Foo{5}.
- gpderetta 2y agoIIRC for a brief period GCC considered values outside of the enumeration as UB and heavily optimized according to this. At some point this interpretetion made it as far as at least a draft standard. Then it got reverted as it went against decades of common usages and instead the opposite was made explicit in the standard. Making it UB for enum classes was considered, but the strongly typeded alias use case was considered safer and more useful.
- account42 2y agoUnfortunate. Then again, constraint value integer types are something that would be useful in general and perhaps can be provided independently of enum class.
- Sharlin 2y agoUf, I see, thanks. I could’ve sworn that this was one of the things enum class was supposed to fix.
- MawKKe 2y agoIMO that's the typical experience with many of the features in modern C++ standards. You read about a really neat useful thing they added, something that seems to provide a safe and practical way to overcome a shortcoming in the language. You may even get a little excited...until you try to actually use it and realize its full of new footguns and weird limitations
- fgkhax 2y agoYes, you read about std::variant on a blog and think that it is a sum type. Then you try it out and realize that it's a thin (type-safe) wrapper over tagged unions that is at least three times slower and has about 5 unreadable alternatives that replace simple switch statements. Then you find out that members of a "variant" are not really variant members but just the individual types that can be assigned to a union. For example, assigning to a non-const reference does not work (and obviously cannot work once you realize that std::variant is just syntax sugar over a tagged union). Most of these new additions since C++11 are just leaky abstractions and wrappers.
- galkk 2y agoOhh, and to make the use of a variant to look like pattern match over type you need to copy paste some template magic. https://schneide.blog/2018/01/11/c17-the-two-line-visitor-explained/ https://schneide.blog/2018/01/11/c17-the-two-line-visitor-ex... variants are a such disappointment at every step of trying to use them
- nly 2y agoA few code snippets of what you see as weaknesses of std::variant may be appropriate, as I couldn't figure out your complaint. Assigning to a variant taken by non-const& works fine for me. I personally would have liked to see recursive variant types and multi-visitation (as supported by boost::variant).
- amluto 2y agoI think the comment means: std::variant<int&, etc> does not work well.
- layer8 2y agoThe main difference between old ("unscoped") and new ("scoped") enums, besides dropping implicit conversions from/to integral types, is the scope of the named constants. With unscoped enums, the constants are in the surrounding scope, which means that constants with the same name but of different enum types collide with each other. One solution is to wrap them in a dummy struct or class (`struct Foo { enum Bar { baz = 0 } }` => `Foo::baz`). Scoped enums are more or less a shortcut for that, where with `enum class Foo { bar }` you have to write `Foo::bar`, or you can use `using enum Foo` to be able to write plain `bar` again. The reason other values of the underlying integral type beyond the named ones are allowed, is that enums are often used to specify bit values or bit masks that you AND/OR with each other. It is what it is. The other question is, how would you go about converting an integral value to an enum type if only specific values were allowed? Either you need an explicit case distinction (potentially large switch) for all the allowed values, or there would have to be a hidden structure/array specifying the valid values at runtime, generated by the compiler. C++ is rather conservative in adding such runtime structures.
- anonymoushn 2y agoWhile I'm not necessarily sold on making tons of stuff UB in release builds and checked in debug builds, it seems maybe better than not having exhaustive enums at all. Here's an example of this feature from some other language: https://ziglang.org/documentation/master/#enumFromInt https://ziglang.org/documentation/master/#enumFromInt
- account42 2y ago> The reason other values of the underlying integral type beyond the named ones are allowed, is that enums are often used to specify bit values or bit masks that you AND/OR with each other. It is what it is. Except bitwise operations of orign enum values yields the underlying type and not an enum value. And for enum classes the operators don't exist at all. So you need to write custom operators(or manual casts) for this use case anyway so you might as well go all the way and write a proper typed bitset type instead of abusing enums. Allowing this for old enums makes sense for C compat but that doesn't mean enum class couldn't have been stricter.
- zarzavat 2y agoWhat the alternative? Let’s say you have a file, you parse a uint32_t, and you want to convert that into the Handle type. If the enum is closed how do you do it? Giant switch? That would break the fundamental principle that C++ abstractions are zero-cost.
- anonymoushn 2y agoI don't think that "abstractions are zero cost" necessarily applies to every serialization format you could choose to use, or to any other thing that isn't part of the language. If you want to guarantee that some value is within the allowed range of values, normally you'd have to run some code to do that, yes.
- zarzavat 2y agoIt applies to the enum. If you couldn’t lift an integer into the enum without branching, then the enum isn’t zero cost - it requires more resources than C would to do the same thing. Bounds checking is never mandatory in C++. When C++ has broken this rule, developers have tended to turn those features off or ban them in style guides.
- anonymoushn 2y agoProviding exhaustive enums wouldn't prevent the language from providing non-exhaustive enums or users from placing values into them cheaply using memcpy.
- HelloNurse 2y agoWe could allow zero cost non-conversion from the primitive type (and invalid values at rest as a consequence) but check and complain about invalid enum values at runtime, "opt-in", when we care: in some kind of novel exhaustive pattern matching statement (not an old school switch statement), which can simply treat invalid values as an erroneous default case, or by invoking a synthesized validation method of the enum class (and if possible with validating variations of conversion from the primitive type, copy constructors and the like). Most enum use is served adequately by never letting in potentially invalid values from untrusted input; an enum variable that is set to an enum constant will necessarily have a valid value. "Decayed" primitive values, like a bitmask formed by bitwise operations between valid enum values, aren't normally intended to be re-ingested as enum values.
- gpderetta 2y agoThat's by design. Consider mapping a file or reinterpret casting a network buffer that contains a structure with such an enum: if has been written by a different version of an application, the possible enum values might be different. That was considered, among other thing, an important use case to support. You can easily build your own safe enum on top of you really want. Edit: someone else pointed out the bitmask use case else thread. Strongly typed integrals is also a common use case.
- maccard 2y agoYou can justify the escape hatches for every feature in C++. The problem is that we eschew sensible defaults to handle the edge cases. Without going into a rust war, I think rust's unsafe is a great way to handle this - for 99% of use cases, you _really_ don't want to put an invalid enum value in there. But, in the number of cases where you do, you should have an escape hatch to do so. If you could do: my_enum_type foo(std::vector<char>& buf, int& offset) { // bounds check omitted for brevity // return my_enum_type{buf{offset++]}; // outside an unsafe-style block would cause a compile error, or a throwing constructor. TBD std::unsafe { return my_enum_type{buf[offset++]}; } } you would reduce the possible impact areas to places where you explicitly want to do dangerous stuff. Instead we end up with a feature that is a "zero cost abstraction" which just pushes the bookkeeping onto the user - every switch statemeent needs to handle the case where someone has passed in 42.
- gpderetta 2y agoI assure you I'm very critical of C++ bad defaults. But in this case I think it was the right solution. There were three options: 1. make invalid values UB. 2. make invalid values non-representable by enforcing checks. 3. enum class is just a strong integer typedef with named constants. Luckily 1 was reject: already too much UB. 2 would require runtime checking and was not considred viable by many; also it would prevent a lot of useful cases. 3 was the remaining option and was consistent with existing enum usage.
- 2y ago
- Lvl999Noob 2y agoI don't know C++ too deeply but from the little bit in the article, it seems like these `enum class` enums are actually Newtypes with associated constants instead of actual enumerations. They clearly don't 'enumerate' all possible values but they do give different behavior from their underlying type. Is it possible to override the constructor so it limits the allowed values? Implement other methods on the `enum class`? If so then that makes them still very useful, albeit not in the usual `enum` sense.
- gpderetta 2y ago> `enum class` enums are actually Newtypes They indeed are. I find myself using enums classes as strong typedefs more often than actual enums.
- zelphirkalt 2y agoWhy is an enum a class? What does that even mean semantically? Why can't an enum simply be ... an enum? Even if under the covers an enum is/was implemented as a special class, why would the language syntax leak this implementation detail to the programmer? Perhaps it would be cleaner to make enum its own concept, rather than trying to shoehorn it into classes. It is a fundamentally different thing than a class after all. Is there a reason, why this is not done?
- tialaramex 2y agoBecause C++ has no way to compatibly adjust its syntax, it's important to them to reuse keywords where possible. So enum and class were two existing keywords whereas scoped would be a new keyword. There's also a sentiment among C++ proponents that classes (a user defined product type with implementation inheritance) are all you really need anyway.