Effective Modern C + + - Item 10: tend to use bounded enum instead of unbounded enum

  • Generally, the visible range of a name (name, or identifier to be precise) declared in braces is limited to braces. However, enum in C++98 is different: its internally defined name will be leaked to the region defining enum itself. Therefore, it is also named unscoped enum:
enum Color { black, white, red };
auto white = false; // VS error: main::white redefine
  • scoped enums in C++11 version are defined by enum class, which will not lead to name disclosure:
enum class Color { black, white, red };
Color c = white;		// error, white not found
Color c = Color::white;	// ok
auto c = Color::white;	// ok
  • The second advantage of scoped enums is that the enumeration value is strongly typed and will not be accidentally implicitly converted to integer (or even then converted to floating point). If you really want to convert, use the most correct method to call cast:
enum class Color { black, white, red };

Color c = red;			// error
Color c = Color::red;	// ok

if (c < 14.5) {						// For enum, it is implicitly converted to double; Compilation failed for enum class
	auto factors = primeFactors(c);	// For enum, it is implicitly converted to int; Compilation failed for enum class
}
// Through static_cast performs explicit type conversion and compiles
if (static_cast<double>(c) < 14.5) {
	auto factors = primeFactors(static_cast<int>(c));
}
  • There is also a view that the third advantage of scoped enum is that it supports forward declaration, but unscoped enum does not, which is wrong in C++11.
  • First of all, the reason why unscoped enum in C++98 does not support forward declaration is that the compiler is for memory efficiency, You want to find an underlying type that can be used for this enumeration type and takes up as little space as possible. For example, in the above example, Color has only three enumeration values, the compiler may choose char instead of int or even long. However, the value of each name in the enumeration can be customized, such as:
enum Status {
	good = 0,
	failed = 1,
	incomplete = 100,
	corrupt = 200,
	indeterminate = 0xFFFFFFFF
};
  • If you can't see the full definition of enumeration, the compiler can't decide what underlying type should be used, Therefore, C++98 only allows enumeration definitions and not enumeration declarations. However, this disadvantage is that, for example, the above State may be a class that many parts of a large system depend on. Once we need to modify it because of a small part (for example, adding a State) will cause the whole system to have to be recompiled! This is a problem that forward declaration can perfectly solve.

  • The solution of C++11 is to give scoped enum a default underlying type int, or you can indicate the type to be used when declaring to override. The latter also applies to unscoped enum.

enum class Status;					// scoped enum is declared forward, and the underlying type is int

enum class Status : std::uint32_t;	// scoped enum is declared forward, and the underlying type is std::uint32_t
enum Color : std::uint8_t;			// unscoped enum forward declaration. The underlying type is std::uint8_t

// scoped enum specifies the type + definition
enum class Status : std::uint32_t {
	good = 0,
	failed = 1,
	corrupt = 100
};
  • Note: here, the author found that unscoped enum can be compiled in VS without adding the underlying type. From the test results, it also uses int as the default type. However, in GCC, the same declaration will report a compilation error.

  • Although scoped enum has the above advantages, the author also mentioned that unscoped enum is very useful in some occasions, such as taking elements from std::tuple of C++11, which will make the syntax concise and clear:

using UserInfo = std::tuple<std::string,     // user name
                            std::string,     // User mailbox
                            std::size_t>;    // Reputation value
UserInfo uInfo{ "1", "2", 3 };

auto& val1 = std::get<1>(uInfo);  			// Do you really remember which number corresponds to which member?

enum UserInfoFieldsUnscoped { uiName, uiEmail, uiReputation };  // Define the auxiliary unscoped enum
auto& val2 = std::get<uiEmail>(uInfo);                          // Implicit conversion using unscoped enum (converted to size_t)

enum class UserInfoFieldsScoped { uiName, uiEmail, uiReputation };                      // If you switch to scoped enum
auto& val3 = std::get<static_cast<std::size_t>(UserInfoFieldsScoped::uiEmail)>(uInfo);  // are you serious??
  • If you really want to use the following method, you can write a general auxiliary function to convert the scoped enum's own type to its underlying data type. The method is to use std::underlying_type, which can extract the underlying type of the enumeration class (belonging to type traits). According to the description in Clauses 14 and 15, we should also declare it with constexpr and noexcept. The results are as follows:
template<typename E>
constexpr auto toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

auto& val3 = std::get<toUType(UserInfoFieldsScoped::uiEmail)>(uInfo);	// A little more concise, maybe

summary

  1. The C++98 style enum is now called unscoped enum.
  2. The enumeration value of scoped enum is visible only inside it. It can only be explicitly converted to other types through cast.
  3. Both enums support the specification of underlying data types. The default type of scoped enum is int, and unscoped enum has no default type.
  4. scoped enum can always be declared forward, and unscoped enum can only be declared forward when indicating the underlying type. (the author's test results show that unscoped enum can be declared forward without specifying the underlying type in MSVC, and int is used as the underlying type by default; it is consistent with this description in GCC.)

Keywords: C++ Back-end

Added by Bullet on Wed, 22 Dec 2021 04:40:52 +0200