5 minutes
Prefer Scoped Enums over Unscoped Enums (Notes)
Scoped vs Unscoped Enums
- General rule: declaring a name inside curly braces is limited to that scope.
- Exception: C++-98 style Enums
NOTE
My notes on Chapter 3, Item 10 of Effective Modern C++ written by Scott Meyers.
Some (or even all) of the text can be similar to what you see in the book, as these are notes: I’ve tried not to be unnecessarily creative with my words. :)
// You can't declare black, white, red in the scope containing the enum Color
enum Color {
black, white, red;
};
auto white = false; // error: white already declared in this scope
- Unscoped Enums have implicit type conversions for their enumerators.
- Enumerators can implicitly convert to integral types, and then to floating-point types.
// Assume Color is declared like above
Color c = red; // valid since Enumerator white is leaked to the scope Color is in
if (c < 10) { // valid, implicit conversion
// ... do something
}
else if (c < 10.5) { // also valid, implicit conversion
// ... do something
}
The C++-98 Style Enums are termed as Unscoped Enums (because of leaking names).
C++-11 Scoped Enums:
// black, white, red are now scoped to Color Enum
enum class Color {
black, white, red;
};
// This is now valid
auto white = false;
Separately, if you do: (consider Color
Enum has already been declared)
Color c = white; // error: no enumerator named "white" is in this scope
Color c = Color::white; // valid
auto c = Color::white; // valid
- Also referred as enum classes (because declared using
enum class
). - Enumerators in scoped Enums are strongly typed (no implicit type conversion)
// Assume Color is declared as above using enum class
Color c = Color::red;
if (c < 10.5) { // Error! can't compare Color and double
// do something...
}
Note: you can do explicit casting using cast
.
Note about enums in C++:
* Every enum in C++ has an integral underlying type that is determined by compilers.
* Compilers need to know the size of enum before using it.
C++98 vs C++11 on Enums
C++98:
- Unscoped enums can not be forward-declared.
- Hence only enums with definitions are supported.
- Allows compilers to choose underlying type for each enum prior to the enum being used.
- Drawbacks?
- Increase in compilation dependencies: wherever the enum is used, even if not affected by any addition in the enum, it will be recompiled (generally speaking, that is without any tweaks/optimizations).
C++11:
- Both unscoped and scoped enums can be forward-declared. Unscoped enums will need a few efforts though:
/* For Scoped Enums */
// Default underlying type is int
enum class Status;
// Override it
enum class Status: std::uint32_t;
/* For Unscoped Enums */
// There is no underlying type for unscoped enum
// You can manually specify though
enum Status: sd::uint32_t;
These specifications for underlying types can also go on enum’s definitions.
Unscoped Enums over Scoped Enums?
Imagine when you have a code like this:
// Ordered as: name, email, reputation
using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
// This is not clear to the reader, you can't always remember what 1st indexed field in UserInfo is
auto val = std::get<1>(uInfo);
Using the property of intrinsic conversion in unscoped enums, you can solve this:
enum InfoFields { uName, uEmail, uReputation };
// UserInfo defined as above
UserInfo uInfo;
// Implicit conversion of int (default underlying type of enums) to std::size_t (that's what std::get takes)
auto val = std::get<uEmail>(uInfo);
For scoped enums though, you’ll have to use static_cast<std::size_t>(InfoFields::uEmail)
instead of just uEmail
(for unscoped enums) passed to std::get
, which is less readable. But…
This can be redued by using a custom function which:
* takes: enum
* returns: corresponding std::size_t
value
std::get
is a template, and the value needs to be understood during compilation only, so the function should be a constexpr
(more on this later in the series).
To generalize, let’s keep the enum’s underlying type (std::underlying_type
type trait)
// Using noexcept because we know there'll be no exceptions raised
template <typename E>
constexpr typename std::underlying_type<E>::type toUType(E enumerator) noexcept {
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}
From the previous blog, we know that in C++14, we could have simplified by writing:
// Using noexcept because we know there'll be no exceptions raised
template <typename E>
constexpr std::underlying_type_t<E> toUType(E enumerator) noexcept {
return static_cast<std::underlying_type_t<E>>(enumerator);
}
Could have used auto
for return type in C++14:
// Using noexcept because we know there'll be no exceptions raised
template <typename E>
constexpr auto toUType(E enumerator) noexcept {
return static_cast<std::underlying_type_t<E>>(enumerator);
}
Now this can be used as:
// Reminder, InfoFields was defined as:
enum InfoFields { uName, uEmail, uReputation };
// toUType is defined above
auto val = std::get<toUType(InfoFields::uEmail)>(uInfo);
Good Reads
- Forward Declaration:
- Are Unscoped Enums still helpful?
- Proposal for forward declaration to enums (accepted), dated 2008: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf
- Forward Declaring an Enum in C++?
Acknowledgement (Reviews)
Thanks to Kshitij Kalambarkar for helping in reviewing the blog. It’s always helpful to get another set of eyes to what you write. :)
That’s it for this blog, thank you for reading everyone!