Enum class improvements for C++17, C++20 and C++23
cppstories.com
cppstories.com
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.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.
It would be easy to make it work, there isn't necessarily a strict relation between the template parameter and the actual stored object. Not having reference variant members was a conscious decision, same as optional<T&>. Hopefully this will be fixed in the future.
I personally would have liked to see recursive variant types and multi-visitation (as supported by boost::variant).
# type foo = Int of { n : int } | Float of { f : float };;
type foo = Int of { n : int; } | Float of { f : float; }
# Int { n = 10 };;
- : foo = Int {n = 10}
# let r = ref (Int { n = 10 });;
val r : foo ref = {contents = Int {n = 10}}
Notice that the constructor Int { n = 10 } automatically produces a foo type and assigning to a mutable ref works.The same in C++, using assignment to a pointer to avoid the lvalue ref error that is irrelevant to this discussion:
#include <variant>
struct myint {
int n;
myint(int n) : n(n) {}
};
struct myfloat {
float f;
myfloat(float f) : f(f) {}
};
using foo = std::variant<myint, myfloat>;
int
main()
{
const foo& x = myint{10}; // works
foo *z = new myint{10}; // error: cannot convert ‘myint*’ to ‘foo*
}
As stated above, this obviously cannot work since C++ has no way of specifying a myint constructor that -- like in OCaml -- automatically produces the variant type foo.C++ would need true algebraic data types with compiler support (that would hopefully be as fast as switch statements). To be useful, they would need a nice syntax and not some hypothetical abomination like:
using foo = std::variant<myint, myfloat> where
struct myint of foo { ... }; std::variant<int&, etc>
does not work well. std::variant<int*, ...>
References in C++ are just sugary pointers.void test(int& y){}
int main() { int* x = nullptr; test(*x); }
You could design a language feature where integer to enum is checked, but that's not enum.
Enum classes already add scoping, forbid implicit conversions and allow explicit underlying types. Those are pure extensions. Making undeclared values invalid or UB would be very surprising to people used to normal enums.
What I have seen is automatic variables escaping their scope as a reference, which rust protects against. And is also much more dangerous, because dereferencing a nullptr is defined behavior on most platforms
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
template<E>
std::optional<E>
from_underlying(std::underlying_type<E>)
requires(std::is_enum_v<E>)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}.Making it UB for enum classes was considered, but the strongly typeded alias use case was considered safer and more useful.
enum Event<T> {
System(SystemEvent),
User(T),
}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.
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.
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.
They indeed are. I find myself using enums classes as strong typedefs more often than actual enums.
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.
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.
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.
Bitmask enums are also a hack and the better solution is to have different types for the individual flags (an enum) as well as a type for combinations of flags (custom type built around an integer).
Yes, these are existing uses but when enum class was designed there were no existing usages for that.
> but when enum class was designed there were no existing usages for that.
IIRC diverging from standard enum was considred, but rejected as deemed a surprising change in behaviour.
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.
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.
False:
enum Foo { foo, bar };
auto x = foo|bar;
static_assert(std::is_same_v<decltype(foo), Foo>);
edit: I'm wrong, see below.In retrospect it makes sense, there is no operator| for enum, it is just calling the int version after implicit conversion.
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.
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.
When C++ has broken this rule, developers have tended to turn those features off or ban them in style guides.
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?
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.
So, assume C++ 29 has pattern matching. That causes people to attempt various idiomatic constructions from languages which always had pattern matching, and of course they're all rather awkward in C++. I think that realisation might point towards an actual language sum type, possibly via abortive attempts to "fix" matching and/or some of the pseudo-sum types provided by the C++ stdlib. So then you'd expect proposals in C++ 32 or C++ 35.
If what I need is a bookshelf then it's true that a wardrobe, a cardboard box, a Kindle, and a shredder are all different things and a bookshelf would not substitute for any of them, but giving me the wardrobe, cardboard box, Kindle and shredder doesn't solve my problem, I needed a bookshelf.
I don't think I've ever seen an analogy contribute to a discussion, and yours is no exception.
sum Expr { Int; Add(Int,Int) }
VS
class Expr { }
class Add extends Expr { Expr left; Expr right; }
std:variant<int,std::string> sum_type_instance = 5;The most common example of a sum type is the "Expression problem" - please read some literature before commenting on a topic.
(Btw, it's called sum type for a reason: summation. The cardinality of the sum type is the sum of the cardinality of its variants)
Tagged unions are an implementation and one that's often a poor fit for the problem. Sum types are an idea from type theory, rather than an implementation detail. It's very on-brand for C++ to have standardized a poor implementation detail rather than the useful idea.
Look at how much hoop jumping was required to make std::optional<T&> work for C++ 26 and then compare how Rust's Option<&T> isn't even special, that's just naturally what happens.
> In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type, or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. [1]
C++ class inheritance is not a sum type for a couple reasons:
- It does not allow you to discriminate on the type.
- You can add RTTI, which allows you to discriminate, but then it is not a fixed set of types.
Obviously, Rust does sum types better than C++, but that is really irrelevant.
struct Add {} std::variant<Add, int> Expr;
OR
class Expr {} class Add : Expr {}
Out of context, this is a very funny thing to say.
enum class Handle : uint32_t { Invalid = 0 };
// process({10}); // error
process(Handle{10});
> In C++14, you could use process(static_cast<Handle>(10)); so, as you can see, the C++17 version is much better.100% disagree that the c++17 version is better. It just lets you right broken code that doesn't look broken.
auto value = std::to_underlying(p); // C++23
is "more expressive" than uint8_t value = static_cast<uint8_t>(Permissions::Read);
The latter seems clearly more expressive, not less. You could tighten it up a bit by using auto on the LHS instead of the redundant uint8_t. In the former I don't know what's going on and I have to go read another header to figure out the type of `value`.Matthias Felleisen
https://jgbm.github.io/eecs762f19/papers/felleisen.pdf
Abstract
The literature on programming languages contains an abundance of informal claims on the relative expressive power of programming languages, but there is no framework for formalizing such statements nor for deriving interesting consequences. As a first step in this direction, we develop a formal notion of expressiveness and investigate its properties. To validate the theory, we analyze some widely held beliefs about the expressive power of several extensions of functional languages. Based on these results, we believe that our system correctly captures many of the informal ideas on expressiveness, and that it constitutes a foundation for further research in this direction.
The expressiveness of an expression allows it to be used in more contexts, such as in generic code where the underlying type may not be known.
Linked Data triples have (subject, predicate, object) and quads have (graph, subject, predicate, object).
RDF has URIs for all Subjects and Predicates.
RDF Objects may be URIs or literal values like xsd:string, xsd:float64, xsd:int (32bit signed value), xsd:integer, xsd:long, xsd:time, xsd:dateTime, xsd:duration.
RDFS then defines Classes and Properties, identified by string URIs.
How best to get from an Enum with (type,attr, {range of values}) to an rdfs:range definition in a schema with a URI prefix?
Python has dataclasses which is newer than attrs, but serde also emits Python pickles
Since the 90's, Turbo Vision, OWL, MFC, CSet++, Tools.h++, VCL, ATL, PowerPlant, Qt, Boost, JUCE, Unreal, CryEngine,....
Maybe if reflection does indeed land into C++26, something might be more universally adopted.
Wt:Dbo years ago but that's just objects to and from SQL, it's not linked data schema or fast serialization with e.g. Arrow.
There should be easy URIs for Enums, which I guess map most closely to the rdfs:range of an rdfs:Property. Something declarative and/or extracted by another source parser would work. To keep scheme definitions DRY
:Thing > https://schema.org/name , :URL , :identifier and subclasses
Thing > Intangible > Enumeration: https://schema.org/Enumeration
Haven't looked at JSONLD much, but it seems like it could be added but would be a library above I think. Extracting the mappings is already doable and is done in the JSON Schema export.
include <daw/json/daw_json_schema.h>
std::string daw::json::to_json_schema<MyType>( "identifier", "title" );
From https://westurner.github.io/hnlog/#comment-38526588 :> SHACL is used for expressing integrity constraints on complete data, while OWL allows inferring implicit facts from incomplete data; SHACL reasoners perform validation, while OWL reasoners do logical inference.
- "Show HN: Pg_jsonschema – A Postgres extension for JSON validation" https://news.ycombinator.com/item?id=32186878 re: json-ld-schema, which bridges JSONschema and SHACL for JSONLD
set_player_color(player, .RED);
That should be the way to use them, it's concise and typesafe set_color(Color::RED);
Why repeat yourself?It's one of the things I love about Swift and Zig
int main() {
using enum ComputeStatus;
ComputeStatus s = NotEnoughMemory;
}
now you have polluted the scope.. C++ have lost the plot.. they understood the issue, but they came up with a solution that's worse...Actually they haven't. Using glob imports inside small functions can enhance readability without causing confusion. Otherwise any kind of aliasing and importing would be polluting the scope since they are bringing other items into your module without the full path.
Usually in an entire codebase or a module there's a lot of competing, similar symbols. But in a function that's not the case, because they do something small. So there's no benefit to being more "explicit", because the function already states what it does.
For example you might not want to use std::chrono. But in, say, a series of timing functions you really don't want to be writing std::chrono:monotonic_clock::now(), do you? You can just use a using and then just call now() or whatever.
computation_service.set_status(ComputeStatus::NotEnoughMemory);
system.set_state(SystemState::Failure);
is a lot clearer than computation_service.set_state(.NotEnoughMemory);
system.set_state(.Failure);
in my opinion. Both objects/structs/whatevers can have a state type of their own but the shorthand form does not indicate whether or not the two can be used interchangeably. As a workaround, you could rename the setter to set_computation_state and set_system_state but then you're just repeating yourself in a different manner that's not as precise.In your example, to me, the value is obfuscated, I prefer conciseness, it's easier to read and it makes more sense
Besides, it encourages developers to better name their variables/functions
SwiftUI made me appreciate it more