Giter VIP home page Giter VIP logo

Comments (13)

aantron avatar aantron commented on August 31, 2024 1

With the commit above, you could create a wrapper header that looks like this:

#define BETTER_ENUMS_DEFAULT_CONSTRUCTOR(Enum) \
  public:                                      \
    Enum() = default;

#include <enum.h>

Then, use that in place of enum.h in your code base. The following program will then compile:

#include <customized_enum.h>
#include <cstdio>

BETTER_ENUM(Foo, int, a, b)

enum class Bar {c, d};

int main()
{
    Foo foo;
    Bar bar;

    printf("%i %i\n", foo._to_integral(), static_cast<int>(bar));

    return 0;
}

This prints garbage such as 1621467190 32767 when I run it. Using value initialization on the variables (Foo foo{}, Bar bar{}) results in the output 0 0 instead, as desired.

from better-enums.

aantron avatar aantron commented on August 31, 2024 1

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 is_valid, here is one overload that is already present: is_valid.

from better-enums.

aantron avatar aantron commented on August 31, 2024

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?

from better-enums.

rcdailey avatar rcdailey commented on August 31, 2024

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 default case if it is zero initialized and there is no corresponding enumerator with the value 0.

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 case statements.

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.

  • Enum mappings between string and enumerator should be automatic and transparent
  • When adding, modifying, or removing an enumerator, the corresponding change to the string mapping should be automatic.
  • Automatic << and >> operators should be defined for said enumeration.

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.

from better-enums.

aantron avatar aantron commented on August 31, 2024

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:

  • I can't make Better Enums a full drop-in replacement for built-in enums anyway, due to some type trickery that I have to do, unless I switch to a type traits approach (see branch traits and here – although note, I plan to simplify the implementation, so some reasons listed against type traits will no longer apply).
  • Committing to this default constructor behavior makes Better Enums much less useful for other use cases – the ones in which programs maintain the invariant that no invalid enum value exists during execution. In this view, built-in enums are broken for more reasons than just poor serialization support – they are too lax and nowhere near sufficiently type-safe.

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:

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.

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.

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 default case if it is zero initialized and there is no corresponding enumerator with the value 0.

If you have a Better Enum with value 0 and there is no corresponding enumerator and case, that is exactly the behavior you will get.

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 case statements.

What do you mean by this? The implicit conversions to integers that are allowed by the code that supports case? Apart from that, Better Enums are classes, yet have all the remaining type safety of C++11 scoped enums, and some more on top, and are still usable in case. Converting to integers is annoying, and I'd love to get rid of it, but the great danger is convertibility from integers.

My goals were much simpler than yours. I did not need the numerous safety checks you provided

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?

The most common problem to solve and the thing that incurs the most maintenance burden when it comes to enumerations is serialization.

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:

  • add default constructors that leave the enum value uninitialized, and make it possible to opt out of this feature for people who don't want that, or
  • modernize the traits branch so that you can have direct access to built-in enums, with serialization and validation functions on the side.

from better-enums.

rcdailey avatar rcdailey commented on August 31, 2024

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?

from better-enums.

aantron avatar aantron commented on August 31, 2024

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, switch. Examples of broken introduction forms would be implicit unchecked conversions from integers (not that these were present), casts, _from_integral_unchecked, and the default constructor we are discussing.

Suppose you have such introduction forms. To be truly robust, almost every switch now needs a default case, with obnoxious handling even if the rest of the cases are exhaustive. It is especially annoying because default tends to suppress compilers' exhaustiveness checking, which otherwise makes code more maintainable. Other things also break, such as conversion to string, which now has to be able to fail, and the caller has to deal with the possibility of failure.

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 _from_integral_unchecked. But those aren't implicit unless you implement an implicit conversion, and hopefully you can lint for them. If your project is small enough, you can simply be sure not to use them. So, Better Enums is also only an improvement in type safety.

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 std::aligned_storage. I would change it for large "broken" codebases too, though.

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?

from better-enums.

aantron avatar aantron commented on August 31, 2024

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?

from better-enums.

rcdailey avatar rcdailey commented on August 31, 2024

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.

from better-enums.

aantron avatar aantron commented on August 31, 2024

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 = default default constructor.

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 0. I may be able to support both in some sane way.

from better-enums.

rcdailey avatar rcdailey commented on August 31, 2024

To quote you from your last 2 responses:

it's too easy to get a scoped enum with undefined value by default construction

and

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?

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:

enum class MyEnumType { One=1, Two=2, Three=3 };
MyEnumType the_variable{}; // Value initialization; NOT default construction

the_variable will represent integral value 0 in this case, instead of an undefined value if I had not value initialized it (removing the {} would have this negative effect).

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.

from better-enums.

aantron avatar aantron commented on August 31, 2024

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.

from better-enums.

rcdailey avatar rcdailey commented on August 31, 2024

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 is_valid() method to verify that it properly maps to an enumerator, assuming you don't already have such a thing, or even assuming it is useful.

from better-enums.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.