天天看點

C++ 2011: Strongly-typed Enums

Enumerations are an important feature of C++, but unlike in other languages, enumerations are not type-safe. There are several problems with the enums: they export their enumerators (the members of an enumeration) to the surrounding scope, you cannot specify the underlying type and theenumerators implicitly convert to int. These problems have been addressed in the new standard with the introduction of "enum class", which are basically strongly-typed enumerations.

Problems with traditional enums

The Scope

Enumerations export their enumerators to the surrounding scope. This has two drawbacks. First, it can lead to name clashes, if two enumerators in different enums declared in the same scope have the same name; second, it's not possible to use an enumerator with a fully qualified name, including the enum name. Let's take for instance the following enum:

enum Selection
{
    None,
    Single,
    Multiple
};

Selection sel = Selection::Single;
      

The above line would generate an error with GCC ('Selection' is not a class or namespace) and a warning with VC++ (nonstandard extension used: enum 'Selection' used in qualified name). To make it work you have to drop the qualification:

Selection sel = Single;       

The second problem appears if we define a new enumeration in the same scope, having an enumerator with the same name as one from Selection.

enum Border      
{ None, Flat, Raised, Sunken};

Both enums have an enumerator called None; since they are exported in the surrounding scope, there is a name clash. To fix it we can either change the name of an enumerator, or put the enums in different namespaces.

namespace Selection
{
   enum Selection
   {
      None,
      Single,
      Multiple
   };
}

namespace Border 
{
   enum Border
   {
      None,
      Flat,
      Raised,
      Sunken
   };
}
      

In this case we can write:

Selection::Selection sel = Selection::Single;
      

Of course, one can argue that using the same name for the namespace and the enum is not a good practice, but this is just an exercise, so you are free to name them however you want.

The Underlying Type

It is not possible to specify the underlying type. It is implementation specific, but it has to be an integral type; it should not be larger than int unless the enumerator value cannot fit an int or unsigned int. Let's take for example the following definition:

namespace Selection
{
   enum Selection
   {
      None = 0,
      Single = 1,
      Multiple = 0xFFFF0000U
   };
}
cout << Selection::Multiple << endl;
      

The output is different with different compilers. Compiled with VC++ the program outputs -65536, while compiled with GCC the output is 4294901760. The difference is that VC++ always uses int for the underlying type, while GCC is more adaptive.

Now, some compilers do allow you to specify the underlying type. For instance VC++ allows this since version 2005. MSDN says the definition of an enum has the form:

enum [tag] [: type] {enum-list} [declarator];
      

And you can actually say:

namespace Selection
{
   enum Selection : unsigned int
   {
      None = 0,
      Single = 1,
      Multiple = 0xFFFF0000U
   };
}
      

In which case, printing Multiple will also show 4294901760.

GCC also allows you to specify an underlying type, however, this is actually supported in the standard only in the new C++ 2011 version.

The underlying type leads to another problem: forward declaration is not possible.

enum Selection;

void make_selection(Selection s)
{
}

enum Selection
{
   None,
   Single,
   Multiple
};
      

The reason is that given only the forward declaration, the compiler does not know the underlying size and thus the size of the enumerator. As a result it cannot compile this piece of code. However, if you try to build this in VC++ you'll notice that it actually compiles. The reason is VC++ always uses int (unless specified otherwise) for the underlying type (as indicated earlier) so it does know the size of the enumerator.

Conversion to int

The values of enumerators implicitly convert to int.

int s1 = None;
      

The problem arises when you try to do something like this:

enum Selection
{
   None = 0,
   Single = 1,
   Multiple = 2,
};

void process(int value)
{
   /* do something */
}

process(Multiple);
      

It doesn't matter what function process does; the point is it was supposed to process an int (maybe any value), not Selections. But it slipped into the code and the compiler shows no warnings and the program runs. One could probably figure other cases when the implicit conversion to int is not desired.

Strongly-typed Enums in C++ 2011

The new standard brings a new kind of enums (the existing ones are left mostly untouched - see below), introduced with 'enum class' and called strongly-typed enums. They no longer export their enumerators to the surrounding scope, can have user specified underlying type of an integral type (also added for tranditional enums) and do not convert implicitly to int.

The scope

The Selection enumeration shown earlier would be defined as:

enum class Selection
{
    None,
    Single,
    Multiple,
};
      

To use it, one must use the Selection:: qualifier, because the enumerators are no longer exported to the surrounding scope (in this case the global namespace).

Selection s = Selection::Multiple;
      

And if we add the Border enumeration, the problem with the redefinition of None would no longer happen, since the two None names are members of different types (in different scopes).

enum class Border
{
    None,
    Flat,
    Raised,
    Sunken
};
      

The Underlying Type

By default, the underlying type is int and the same rules as for traditional enums apply if the user does not specify anything else. However, it is possible to do that now, but you can only use an integral type for that.

enum class Selection : unsigned char
{
    None,
    Single,
    Multiple,
};
      

Since the underlying type can be specified, it will be possible to do forward declaration for enums (for the reasons explained in an earlier paragraph).

enum class Selection : unsigned char;

void make_selection(Selection s)
{
}

enum class Selection : unsigned char
{
    None,
    Single,
    Multiple,
};
      

However, it is possible to specify the underlying type for traditional enums too. This is also legal in C++ 2011 (and actually supported for some years by various compilers):

enum Selection : unsigned char
{
    None,
    Single,
    Multiple,
};
      

Forward declaration with traditional enums will also be possible.

Implicit Conversion

Enumerators of strongly-typed enums no longer implicitly convert to int. The following piece of code will trigger an error:

int s = Selection::Multiple;
      

Instead, you have to write:

Selection s = Selection::Multiple;
      

This would allow the compiler to immediately flag cases when the enumerators are used in place of an int (as shown earlier), which enable us to write better code.

Conclusions

The new C++ standard brings a new type of enums, referred to as strongly-typed enums and introduced with the 'enum class' keywords. They solve the known problems with the traditional enums: the scope of the enumerators, the possibility to specify the underlying type (also added for the traditional enums) and the implicit conversion to int which is not supported.

About the Author

Marius Bancila is a Microsoft MVP for VC++. He works as a software developer for a Norwegian-based company. He is mainly focused on building desktop applications with MFC and VC#. He keeps a blog at www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++/VC++ programmers.

自: http://www.codeguru.com/cpp/cpp/article.php/c19083/C-2011-Strongly-typed-Enums.htm