6

Suppose we have

enum class Month {jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};

Each value is an int, 0 to 11. Then I expect variable of type Month to hold only these enumerated values. So here's the only OK way to create a variable:

Month m = Month::may;

But here are some other ways that language allows:

Month m1 = Month(12345);
Month m2 = static_cast<Month>(12345);

which is somewhat disappointing. How do I allow only the first way? Or how do people cope with poor enums in C++?

xskxzr
  • 12,442
  • 12
  • 37
  • 77
jazzandrock
  • 317
  • 2
  • 10
  • 1
    Possible duplicate of [Why is enum class preferred over plain enum?](https://stackoverflow.com/questions/18335861/why-is-enum-class-preferred-over-plain-enum) – Retired Ninja Feb 25 '18 at 01:34
  • 1
    Obviously, it takes some effort to write the third way, and if you use a cast, you are saying "I know what I'm doing". It would be nice to exclude the second way. –  Feb 25 '18 at 01:38
  • C++ gives you enough rope to shoot yourself in the foot. – Eljay Feb 25 '18 at 02:01
  • @Eljay Mixed metaphors aside, I completely agree. – Blair Fonville Feb 25 '18 at 02:32

4 Answers4

6

How do I allow only the first way?

Not possible with enums.

If you want an idiot proof "enum" that cannot be explicitly converted from (possibly invalid) values, then you can use a full blown class instead of an enum. Unfortunately that involves some boilerplate:

struct Month {
    constexpr int value() noexcept { return v; }
    static constexpr Month jan() noexcept { return 0; };
    static constexpr Month feb() noexcept { return 1; };
    static constexpr Month mar() noexcept { return 2; };
    static constexpr Month apr() noexcept { return 3; };
    static constexpr Month may() noexcept { return 4; };
    static constexpr Month jun() noexcept { return 5; };
    static constexpr Month jul() noexcept { return 6; };
    static constexpr Month aug() noexcept { return 7; };
    static constexpr Month sep() noexcept { return 8; };
    static constexpr Month oct() noexcept { return 9; };
    static constexpr Month nov() noexcept { return 10; };
    static constexpr Month dec() noexcept { return 11; };
private:
    int v;
    constexpr Month(int v) noexcept: v(v) {}
};
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Is this complete or a concept (not in template sense)? Could you add a short use-case? I keep wondering if some magic happens somewhere. Why `constexpr` and `noexcept` on the methods? And why give it a private member that is inaccessible and disallow ctor or setting it, but allow usage? – BadAtLaTeX Jul 29 '23 at 16:54
  • @gr4nt3d `Is this complete or a concept (not in template sense)?` I don't understand what you mean. `Why constexpr` so that the function ca be used in constant expressions. `and noexcept` So that the function can be used in contexts where potentially throwing functions cannot be used. `And why give it a private member that is inaccessible and disallow ctor or setting it, but allow usage?` To achieve the goal that jazzandrock was asking for i.e. to prevent conversion (even explicit) to/from the underlying integer, to and syntactically prevent creation of invalid values. – eerorika Jul 29 '23 at 20:02
  • I meant whether this is to be used as is or extended with more understanding than I am equipped with? About the ctor, well I see the point in disallowing the type conversion (TC), but will the default ctor then initialize `v` properly? Afaik `Month bday; bday.v = 1;` wouldn't work. Could one use `Month bday = { 1 };` and it wouldn't count as the TC-ctor (but initializer-list?)? Or would only `Month bday = {Month::feb()};` // `Month bday = Month::feb();` // `Month bday(Month::feb());` work? – BadAtLaTeX Jul 29 '23 at 21:28
  • 1
    @gr4nt3d `but will the default ctor then initialize v properly` The default constructor will be implicitly deleted. `Could one use Month bday = { 1 };` One couldn't use that. `Or would only Month bday = {Month::feb()}; // Month bday = Month::feb();` Yes. `I meant whether this is to be used as is or extended` I would recommend adding comparison operators. Now with C++20 it's as easy as `friend auto operator<=>(const Month&, const Month&) = default;` Other than that, I don't see anything obvious missing. – eerorika Jul 29 '23 at 23:34
3

Your issue could be solved by using a regular class encapsulating a regular enum, like in the following example:

class Month  {
public:
  enum Type {
    jan, feb, mar, apr, may, jun,
    jul, aug, sep, oct, nov, dec
  };
  Month(Type t);
private :
  Type type;
};

then the following would produce compile-time errors:

  Month mm = Month::jan;
  Month m1 = Month(12345);
  Month m2 = static_cast<Month>(12345);

  e.cpp:27:25: error: invalid conversion from 'int' to 'Month::Type' [-
  fpermissive]
      Month m1 = Month(12345);
  e.cpp:26:42: error: invalid conversion from 'int' to 'Month::Type' [-
  fpermissive]
      Month m2 = static_cast<Month>(12345);

There is still a possible, but more complicated scenario to work-around

 Month m1 = Month(Month::Type(12345));

This, however, can be checked dynamically, like this

 Month::Month(Type t) : type(t){
  if (int(t) < 0 || int(t) > int(dec)) {
      throw "error";
  }
}
Serge
  • 11,616
  • 3
  • 18
  • 28
2

Then I expect variable of type Month to hold only these enumerated values

Then you have misunderstood what enums (even scoped enums) are. They give you convenient names for some values of the underlying type (and, in the case of scoped enums, prohibit implicit conversions from that type). They do not restrict an object to those named values, nor are they intended to. If you want to do that, create a class with validation routines in anything that changes its state.

However, generally the overhead of such an approach is not considered worthwhile. Following the usual C++ practice, it is just assumed that you never give your enum a value that you shouldn't. If you did, why? That's a bug: fix it. The prohibition on implicit conversions, provided by scoped enums, should make these bugs vanishingly unlikely. And if someone goes out of their way to explicitly convert an unnamed value? That's their own fault! Document that the behaviour of the program will then be "undefined" (not per the language but per your own code's API) and move on.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • 2
    *"The prohibition on implicit conversions, provided by scoped enums, should make these bugs vanishingly unlikely. And if someone goes out of their way to explicitly convert an unnamed value? That's their own fault!"* - that's not very practical or helpful. The question's example - conversions between month enumerators and integers - is a *very* common and genuinely error-prone requirement e.g. for interaction with existing libraries that expect numbers (sometimes 0-based, sometimes 1-), for calculations, for serialisation and deserialisation. There are enums to worry about, and others not... – Tony Delroy Feb 25 '18 at 04:42
  • @TonyDelroy: You may not think that it is "helpful", but it is true. Would it be less error-prone if there were some other way to satisfy that particular use case? Absolutely! But we do not live in that universe. – Lightness Races in Orbit Feb 25 '18 at 11:49
0

You can't forbid things that the language allows without modifying the language itself. After all, it's perfectly possible to do:

Month m;
int * val = (int *) &m;
*val = -46;

...and nothing will stop you. The key is that people typically simply shouldn't do that, and if they do, they typically have a very good reason for it.

If you want to enforce stronger coding policies, you'll need either a compiler that supports that kind of diagnostics (typically via warning flags, that can often be converted to hard errors) or simply a different language. If that is not acceptable, typically people cope with problems like this one by simply not writing insane code as in the example I posted.

aaaaaa123456789
  • 5,541
  • 1
  • 20
  • 33
  • Your code causes undefined behaviour (strict aliasing violation, there is no exception for aliasing an enum as its underlying type) – M.M Feb 25 '18 at 02:40
  • That's the whole point of the example. Nothing stops you from writing code that causes undefined behavior. – aaaaaa123456789 Mar 02 '18 at 02:15