-
-
Notifications
You must be signed in to change notification settings - Fork 173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enum type should be default constructible #10
Comments
Thanks for this, this is the kind of feedback I need to decide what the default constructor should do. What do you think about the considerations here? |
After reading the section you linked, I feel like maybe you're overthinking the problem. Despite the complex implementation to support what you have today, I think the requirements are simple and the design is already defined for you. IMHO, semantics should be modeled around existing scoped enumeration functionality. This makes the question of whether to have a default constructor obvious: Yes. From a unit test perspective, every possible usage of a zero-initialized scoped enum should also be a valid and passing test for your custom class. That means when you use it in a switch statement, that might mean hitting the As per my SO question you kindly responded to, I had devised a solution that utilized X Macros per one of the suggestions there. It's much uglier (from a usage standpoint) and less functional than what you have, but maybe it will give you some ideas. I have no way of knowing all of the various ideas you have tried to implement, but I figured to keep semantics the same I'd have to somehow make my macro magic define a true scoped enumeration. A class to simulate the behavior just wouldn't work, especially when you need to have type safety for the enumerators as well as make them usable in My goals were much simpler than yours. I did not need the numerous safety checks you provided. The most common problem to solve and the thing that incurs the most maintenance burden when it comes to enumerations is serialization.
These are the only guarantees you can really make without worrying about special cases. For example, if I were to go out of my way to make sure that no enumerators were left out of switch statements for this enum, that would be more of an annoyance because depending on business rules I may not want to check all enumerators and only care about a few, as far as switching goes. If I am to use your better enums library for my needs, I need them to be an absolute drop in replacement for scoped enums. |
I am confused by several statements above. I'll reply first with what seems to be most relevant to the title of the issue. I doubt all users are looking for a drop-in replacement for built-in enums, especially given that this library originated with a user that wasn't. I agree that having no default constructor is surprising and restrictive for C++, and I agree that replacing built-in enums is a valid and common use case. However, I am still looking for stronger arguments before I make such a decision for all users of the library. There are two reasons why I hesitate:
It would be nice to support built-in-enum-like default constructors without causing problems for such users – perhaps as some kind of opt-in or opt-out feature? Regarding the rest:
I am not sure what some of this is referring to. Use cases and the question of a default constructor were discussed above, but the complexity of the implementation has nothing to do with the default constructor question. First, the implementation is not that complex, but what complexity there is has to do with the design limitations of C++, differences between compilers, differences between C++11 and C++98, string conversions, and some experimental features I am going to remove to a branch.
If you have a Better Enum with value 0 and there is no corresponding enumerator and
What do you mean by this? The implicit conversions to integers that are allowed by the code that supports
Fair enough, but other users want the safety checks. By the way, the safety checks fall into one category: making sure you can't convert a string or integer that does not represent an enumerator to a value of enum type. There is no way to avoid doing a check when converting from strings anyway, so that leaves the check when converting from integers and the lack of a default constructor as the only "excess" safety measures that could be removed. The integer check can be opted out of in the current release. So, I don't see what you mean by "numerous" – did I misunderstand what you are referring to?
This is indeed one of the biggest problems, but there is also the cost of detecting validation bugs and maintaining validity checking code throughout the program. In conclusion, I think the best options for supporting your usage would be to either:
|
I think my main holdup (and the comments regarding "complexity" were more related to this) is that you keep mentioning the integer <-> enum implicit conversions. This is true with unscoped enums, but not true with scoped enums. There are huge semantic differences between the two. I prefer the latter, and it's a built-in feature of the language that scoped enums are truly type safe. Is there some other type safety issue you're referring to, even after considering scoped enum semantics and guarantees? |
By "keep" mentioning, I assume you mean in the documentation, not here – right? That is only because I want to be clear and honest about what is missing relative to scoped enums. Believe me, I am aware of the differences between old enums and new enums. Yes, there are remaining type safety issues. Scoped enums are definitely not type safe – just more type safe than old enums. It's a welcome improvement, but still frustrating for some use cases. Specifically, it's too easy to get a scoped enum with undefined value by default construction. That is about as unsafe as an implicit conversion from integer, and a much more serious safety issue than implicit conversion to integer. Contrary to how C++ is defined, the expectation of many people, and the practice in many other languages, is that an enum should have the value of one of its enumerands. Introduction forms that break this expectation, especially silently, break type safety, because they break the main elimination form, Suppose you have such introduction forms. To be truly robust, almost every To correctly avoid these complications in each case where they ought to be superfluous, the user has to be able to reason that only valid values are ever eliminated, and the reasoning has to be repeated each time the program is modified. The point of type safety is to make this reasoning trivial by guaranteeing this property for the user. By comparison with these mistakes, an elimination that can silently convert an enum to a value of a type that is not restrictive is unfortunate, but not as big a deal. That's why I preferred to wrap in a class without a default constructor, at the cost of getting conversion semantics more similar to old enums – though, see this. Of course, according to C++, enums don't have to have the value of one of their enumerands. Scoped enums are pretty much type safe up to this definition. But unscoped enums are type safe up to their definition as well – it's a circular argument. And, these are broken definitions in the opinions of many people, just like lack of serialization is broken in the opinions of many people. And of course, Better Enums are still not type safe even without the default constructor, since there are still casts and It does break some programs (as does forbidding implicit conversion to integer). I am willing to change that in the interest of sane usage. It would be nice to see a definite use case, where the code isn't conceptually broken or can't make use of something like I think imposing a default constructor globally on all programs using this library is a very "brutal" way to solve the problem of deserialization with defaults. You may also be using default constructors in other ways, but my point is you are assigning extra semantics to default constructors by your usage. I would be happy to support you usage somehow, but I don't want to damage other users who don't need these semantics and don't want the other implications of a default constructor. If your code base is not immutable, perhaps you can switch to using a type trait, whose default behavior is to use a default constructor? |
Also, why do you want default construction in this case? Or, what should the default constructor do? If it leaves the value undefined, and the surrounding code doesn't pre-initialize the memory, then how do you distinguish failure from a valid deserialized value? Or is that irrelevant in your code base? |
If you think of enums as just semantic wrappers over integral types (which is really what they are) it makes more sense. Value initializing (i.e. zero initializing) an enum is not undefined behavior as you mentioned. The value zero is within the limits of all numeric types in C++ which does not make the behavior undefined as you mentioned. In the absence or abstinence of exceptions, value initialization is a great way to provide a stub value as a return if serialization fails. At that level, we aren't functioning on enumerations for some business logic. We handle them as primitive types. Now does it makes sense to do anything with an enum that's been zero initialized but zero does not map to an enumerator? Of course not. But those aren't rules I defined. The language defined them. Maybe I'm the one in the wrong here making the assumption that it was your intention for your enums to be a semantic drop in replacement for normal enums. In fact, after hearing your ideals, that does not seem to be the case. Maybe you shouldn't change anything. After all, given what you are trying to do, it doesn't make sense to allow it. |
I didn't mention any undefined behavior at all. I mentioned a defined behavior of a default constructor that leaves the value of the enum undefined. I actually don't think that's even what is needed to get zero-initialization with a class type – I think I would need to have a In any case, do all your enum declarations begin with some sort of invalid value that represents deserialization failure? This is very similar to how Better Enums was used in the project it was originally created for, except there the default value didn't have to have representation |
To quote you from your last 2 responses:
and
Emphasis Mine. "Undefined" has a very specific definition in the standard. Nothing about value initializing an enum leaves it in an undefined state. The standard clearly defines it will be zero initialized when it is value initialized. Also enums are not default constructed. I apologize for initially using this term, it was a mistake on my part. They are value initialized in the very first example I gave you with the template function. To be crystal clear, the specific scenario I'm referring to is:
If you're referring to your own implementation, maybe that's where we're off on the wrong page here. But I'm strictly referring to built in enums. |
Yes, that's why we may be on the wrong page, though I have been following what you are saying, with the exception of you having to remind me that your deserializer would use value-initialization on Better Enums in response to one of my questions. I've been talking about how and whether to make Better Enums default constructible as per the issue title, in order to emulate zero initialization of scoped enums. I've been avoiding restriction to just talking about value initialization or zero initialization because that is a consequence of the site of the constructor call, not a property or function of the constructor (unless the constructor forces zero initialization of course, but that would fail to emulate scoped enums with automatic storage and unspecified value, and also be a rather arbitrary choice for a library to make, for other reasons). Given value initialization is a property of the call site, I cannot directly control zero initialization from inside Better Enums, but I can support it with the right choice of constructors – that's why it makes sense to talk about constructors and the implementation. It's just the next logical step in the conversation, and why I take it there. I do understand what you're saying, what your scenario is, and what is value initialization, and I am not sure why you think that I might not. What I am saying is that what is necessary to support zero initialization for a class type, which is a default constructor of some kind, and probably an implicit one, is not an exact solution for just your scenario. It will produce undesirable effects at other kinds of call sites and in other enum usage patterns. I am giving this as a reason for my reluctance to just support it. I'm also inviting you to consider alternatives, or maybe suggest novel solutions or considerations that I haven't come up with, or correct misconceptions I may have about how to support zero initialization, if you should please to do so. I also don't understand the problem with me mentioning the constructor leaving the value undefined (or perhaps, to be more precise, which value will result from construction is left undefined). Isn't this what the constructor would have to do to support zero initialization? Perhaps I should have called the state indeterminate, but it's certainly not undefined behavior. And, setting context aside, it is what you inherently get when you create a scoped enum without a value. Whether the context forces zero initialization of the enum or not is an entirely separate matter, and some contexts don't. Scoped enums don't have any special default value on their own (unlike objects with an explicit default constructor, for example, which is much harder to avoid calling) – and this is both a part of the type safety issue I was just explaining above, and the reason why emulation with a default constructor is slightly tricky. The only other way I can think of getting zero initialization is to hardcode it into an explicit constructor, but that seems dubious. Is this what you had in mind? So, if you don't mind, let's move along to the practicals – would you prefer Better Enums generate a plain scoped enum so you can have regular C++ semantics, with a parallel traits type you can use for serialization and optional validation? Would you prefer Better Enums generate wrapping classes that support some kind of pluggable default constructor policy? I suppose I can look into implementing the second one and get back to you when I have something that works. |
With the commit above, you could create a wrapper header that looks like this:
Then, use that in place of
This prints garbage such as |
Nice change. Have you given any thought to making this the default behavior? Seems desirable. It would give you the ability to add some sort of |
Yes, and maybe it is ultimately the right thing to do for the vast majority of users. But absent more feedback, I want to remain conservative with respect to type safety. Regarding |
Normal scoped enumerations are default constructible:
However, your
ENUM
macro does not yield a struct that matches these semantics:The error I get is:
These semantics are extremely important for template metaprogramming. Example test case:
Internally, the method above will attempt to stream in a string and map it to a proper enumerator. If the string does not map to a valid enumerator, it will instead set it to the value of
default_value
. This template works just fine for true scoped enumerations, but not with your struct declared by theENUM
macro.The text was updated successfully, but these errors were encountered: