Item 10: Prefer scoped enums to unscoped enums

There is a basic principle that declaring a name in curly brackets restricts the visibility of the name to the scope of the curly brackets. However, enumerations declared in the C++98 style enum do not follow this principle. The visibility of these names extends to the scope containing the enumeration, meaning that the same name cannot be contained within this scope:

enum Color { black, white, red };  // black, white, red are in same scope as Color

auto white = false;                // error! white already declared in this scope

The case of enumeration scope leaks directly spawned an official term: unscoped. Correspondingly, C++11 also has a term: scoped enums, whose scope does not leak:

enum class Color { black, white, red }; // black, white, red are scoped to Color
auto white = false;                     // fine, no other "white" in scope
Color c = white;                        // error! no enumerator named "white" is in this scope
Color c = Color::white;                 // fine
auto c = Color::white;                  // also fine (and in accord with Item 5's advice)

Because scped enum s are declared by "enum class", they are sometimes called enumeration classes.
Reducing namespace pollution alone gives us good reasons to choose scoped enums, but scoped enums have another overwhelming advantage: their enumerations are strongly typed. The unscoped enum enumerator can be implicitly converted to a numeric type (then from a numeric type to a floating point type). In this way, the following strange writing is completely effective:

enum Color { black, white, red};//unscoped enum


std::vector<std::size_t>                        // func. returning 
    primeFactors(std::size_t x);                // prime factors of x

Color c = red;
// ...

if(c < 14.5) {                                 // compare Color to double (!)

    auto factors = primeFactors(c);            // compute prime factors of a Color (!)
    // ...

}

Simply adding a "class" after "enum" converts unscoped enum to scoped enum, and the semantics are suddenly completely different. There is no implicit conversion to any other type of enumeration in scoped enum:

enum class Color { black, white, red };      // enum is now scoped

Color c = Color::red;                        // as before, butwith scope qualifier
...

if (c < 14.5) {                              // error! can't compare Color and double

    auto factors = primeFactors(c);          // error! can't pass Color to 
    ...                                      // function expecting std::size_t
}

If you really want to perform a Color-to-different conversion, use a cast:

if(static_cast<double>(c) < 14.5) {							  // odd code, but it's valid

    auto factors = primeFactors(static_cast<std::size_t>(c)); // suspect, but it compiles
    ...
}

It appears that scoped enum may have a third advantage, and scoped enum appears to be declarable forward without having to take an enumeration in it:

enum Color;         // error!
enum class Color;   // fine

This is misleading. In C++11, unscoped enum can also be declared forward, but requires a little extra work. This is because each enum in C++ has a shaping base type that is determined by the compiler. For unscoped enum, such as Color:
enum Color { black, white, red };
The compiler may choose char as the base type because only three variables need to be expressed. Then, some enum values may have a much wider range:

enum Status {   good = 0,
                failed = 1,
                incomplete = 100,
                corrupt = 200,
                indeterminate = 0xFFFFFFFF
};

These values need to be represented in a range from 0 to 0xFFFFFF. In addition to some unusual machines where a char consists of at least 32 bits, the compiler must choose an integer type larger than char to represent the value of Status. For efficient use of memory, the compiler often chooses the smallest base type as long as one base type successfully represents the range of values of members in enum. In some cases, compilers will swap space for time, in which case they may not necessarily choose the smallest underlying type, but they will certainly also consider space optimization. To achieve this, C++98 only supports definitions (all enum members must be listed); The declaration of enum is not allowed. This allows the compiler to select a base type for each enum before each enum is used.
However, there are drawbacks to not declare enum in advance. Most notably, it may increase compilation dependency. Consider Status enum again:

enum Status { good = 0,
      failed = 1,
      incomplete = 100,
      corrupt = 200,
      indeterminate = 0xFFFFFFFF
};

This enum may need to be used in a system, so it is included in the header file, and then every part of the system needs to depend on it. If a new status value is added,

enum Status { good = 0,
      failed = 1,
      incomplete = 100,
      corrupt = 200,
              audited = 500,    // new enumerator
      indeterminate = 0xFFFFFFFF
};

It is very likely that the entire system will need to be recompiled, even if only a simple subsystem (and even a simpler function) uses this enum. This is hated by people. This is also what the enum predecessor declaration eliminates in C++11 (compilation dependency). For example, here is a statement from scoped enum that is perfectly valid. And a function uses it as a parameter:

enum class Status;                      // forward declaration

void continueProcessing(Status s);      // use of fwd-declared enum

If the Status definition is modified and the header file contains only those declarations, there is no need to recompile. In addition, if Status modifies (for example, by adding an audited member), but the behavior of continueProcessing is not affected (for example, because continueProcessing does not use audited), the implementation of continueProcessing does not need to be recompiled.
But if the compiler needs to know the size of an enum before it can be used, how can C++11's enum use a preconception, while C++98's enum does not? The answer is simple: the underlying type of scoped enum is always known, and for an unscoped enum, you can also specify its underlying type.
Typically, the underlying type of a scoped enum is int:

enum class Status;     //The underlying type is int

If the default does not work for you, you can set it yourself:

enum class Status: std::uint32_t;  //The basic type of Status is
                                   //std::uint32_t

In any case, the compiler knows the size of scoped enum.
To clarify the underlying type of an unscoped enum, you need to do the same thing as scoped enum so that you can make the predecessor statement:

enum Color: std::uint8_t;       //Pre-declaration of unscoped enum
                                //The underlying type is std::uint8_t

The underlying type can also be explicitly defined in enum:

enum class Status: std::uint32_t { good = 0,
  failed = 1,
  incomplete = 100,
  corrupt = 200,
  audited = 500,
  indeterminate = 0xFFFFFFFF
};

The fact that scoped enum avoids namespace pollution and does not make meaningless implicit type conversions may surprise you: there is at least one case where unscoped enum is more useful than scoped enum.
For example, when we use the std::tuple field of C++11. For example, suppose for a user of a social networking site, we want to design a tuple with a name, an email address, and a reputation value:

using UserInfo =
std::tuple<std::string,		//name
          std::string,		//email
          std::size_t>;		//reputation

Indicate each field only with a comment, which will have no effect when encountering a separate source file:

UserInfo uInfo;                  //Objects of type tuple
...

auto val = std::get<1>(uInfo);   //Take the value of field 1

As a programmer, you have many ways to document it. Do you really remember that field 1 represents the email address of the user? I don't think so. Use an unscoped enum to associate names to fields:

enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
...

auto val = std::get<uiEmail>(uInfo);            //Get the value of the email field

For this to work, you must have from UserInfoFields to std::size_ Implicit conversion of t (std::get required type).
Corresponding code implemented with scoped enum can become cumbersome:

enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
...

auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uinfo);

The tedious situation can be reduced by writing a function that passes in an enum member and returns the corresponding std::size_t value, but this is a bit subtle. std::get is a template, and the value you provide is the template parameter (note that angle brackets are used, not parentheses), so the conversion function (enum members to std::size_t) must produce results at compile time. As Item 15 explains, this means it must be a constexpr function.
In fact, it should be the constexpr function template because it should work under any enum. And if we let it continue to be generalized, we should also generalize the return type. Rather than returning an std::size_t, we need to return the base type of enum. Via std::underlying_ Typee can do this. (Look at the type traits information in Item 9) Finally, we declare it noexcept (look at Item 14) because we know it will not produce any exceptions. The result is a compile-time const function template toUType that requires an enum member of any type and returns its value (the type is the base type):

template<typename E>
constexpr typename std::underlying_type<E>::type
	toUType(E enumerator) noexcept
{
	return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

In C++14, by using a powerful std::underlying_ Type_ T (see Item 9) to replace typename std::underlying_type::type,toUType can be simplified:

template<typename E> 	// C++14
constexpr std::underlying_type_t<E>
  toUType(E enumerator) noexcept
{
	return static_cast<std::underlying_type_t<E>>(enumerator);
}

In C++14, the more powerful auto return type (see Item 3) is also valid:

template<typename E> // C++14
constexpr auto
  toUType(E enumerator) noexcept
{
	return static_cast<std::underlying_type_t<E>>(enumerator);
}

Whatever the function is written, toUType allows us to access the tuple field like this:

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

There's still a lot to write here compared to unscoped enum, but it also avoids namespace pollution and conversion you don't realize. In many cases, you may find it reasonable to write a few more words than its trap (but back a long time ago when we were using a 2400-baud modem for digital communication, things were different).
Things to remember
C++98 style enum is called unscoped enum

  • Members of scoped enum are visible only in enum, and they can only be converted to other types when using cast.
  • Both scopeds and unscopedenum support custom base types. The default base type for scoped enum is int. unscoped enum has no default base type;
  • scoped enum can always be predecessored, unscoped enum can only be predecessored if the underlying type is clear;

Tags: C++

Posted by Xil3 on Fri, 19 Aug 2022 03:29:34 +0300