From 9dd41de0fe9d942a4c7835e5e484b2b6b0c6ae36 Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:24:22 +0700 Subject: [PATCH 1/4] =?UTF-8?q?docs:=20rewrite=20enums=20chapter=20with=20?= =?UTF-8?q?C++98=E2=80=9326=20semantics,=20diagnostics,=20and=20reflection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_low_level/07_enums/README.md | 930 +++++++++++++++++++++----------- 1 file changed, 628 insertions(+), 302 deletions(-) diff --git a/01_low_level/07_enums/README.md b/01_low_level/07_enums/README.md index 37299fd..cc7f725 100644 --- a/01_low_level/07_enums/README.md +++ b/01_low_level/07_enums/README.md @@ -1,521 +1,847 @@ -# 07. Enums in Modern C++ +# 07. Enumerations in Modern C++: From C++98 to C++26 ---- +An enumeration is a distinct C++ type with an integral underlying representation. That definition looks simple until one tries to answer which integer values an enum can hold, how it participates in arithmetic, why a seemingly innocuous cast can be undefined behavior, or whether a compiler can prove that a switch covers every meaningful state. The old C-style enumeration was built for named integer constants; modern scoped enumerations are intended to make semantic domains visible in the type system. -## 1. What an enum *is* in C++ +For experienced C++ developers, the difficult questions are not how to spell \`enum class\), but where the language draws the boundaries between an enum's *enumerators*, its *value range*, its *underlying type*, and the *domain* accepted by an application. Those boundaries matter for protocols, bitmasks, overloads, FFI, ABI evolution, optimization, and exhaustive state machines. -* An `enum` is a distinct user-defined type whose values are represented as integers. -* Each enumerator is an integral constant expression. -* An enum object: +This chapter develops the subject historically—from C++98 unscoped enums to C++26 compile-time reflection—while following the four source demonstrations in this directory: - * occupies storage - * has a concrete underlying integer representation - * is *not* just an `int`, even if it behaves like one in some contexts +1. [\`01_enums_core/enums_core.cpp\`](01_enums_core/enums_core.cpp) — unscoped enums, integer promotions, sizes, distinct types, and casts. +2. [\`02_enums_scoped/enums_scoped.cpp\`](02_enums_scoped/enums_scoped.cpp) — scoped enums, explicit representation, forward declarations, and conversions. +3. [\`03_enums_operations/enums_operations.cpp\`](03_enums_operations/enums_operations.cpp) — typed bit flags, overloaded operators, and masking. +4. [\`04_modern_enums/modern_enums.cpp\`](04_modern_enums/modern_enums.cpp) — state decoding, switches, and C++20 \`using enum\`. -```cpp -enum Color { Red, Green, Blue }; -``` +The repository targets C++20. Code from C++23 and C++26 is either shown under explicit feature-test guards or labeled as a later-standard illustration; support for C++26 reflection is not presumed merely because a compiler accepts \`-std=c++26\`. --- -## 2. Unscoped enums (C++98 style) +## 1. An enumeration is a distinct type, not an integer alias + +At the machine level, an enum usually occupies storage in an integer representation. At the C++ type-system level, however, each enumeration declaration introduces a distinct type, even if two enumerations have identical enumerators and the same underlying representation. Confusing those facts leads to broken overloads and accidental cross-domain conversions. + +### Two enums with the same values are still different types + +Consider status and device-state values that happen to share their representation. There is no implicit assignment from one enum type to another, and this is intentional: the number alone does not define its meaning. + +\`\`\`cpp +#include + +enum Status { ready = 0, failed = 1 }; +enum DeviceState { offline = 0, online = 1 }; -### Key properties +static_assert(!std::is_same_v); +// DeviceState d = ready; // ill-formed: distinct enum types +\`\`\` -* Enumerators are injected into the enclosing scope -* Enum values implicitly convert to `int` -* Underlying type is implementation-defined -* Size is platform-dependent -* Arithmetic on enum values is allowed (often unintentionally) +An unscoped enumerator such as \`ready\` participates in arithmetic through implicit integral conversion, but its enumeration type remains distinct. Two enums sharing an underlying type are not interchangeable API domains. -```cpp -enum Status { Ok, Error }; -int x = Ok + 10; // allowed -``` +### Enumerators are compile-time constants, not stored fields -### Pitfalls +Each named enumerator is a constant of the enum type, usable where a constant expression is required. Its value may be implicit or explicitly assigned, and later unassigned enumerators increment the preceding value. -* Name collisions in headers -* Accidental arithmetic -* Silent conversions in overload resolution -* Values outside enumerator set are possible +\`\`\`cpp +enum Error { ok = 0, retry = 4, timeout, abandoned = timeout }; +static_assert(timeout == 5); +static_assert(abandoned == timeout); -```cpp -Status s = static_cast(42); // allowed, but not meaningful -``` +int retry_buffer[retry]{}; // constant bound +\`\`\` + +Nothing about this definition builds a runtime lookup table, reflection structure, or mapping from integers to names. Duplicate enumerator *values* are allowed; duplicate enumerator *names* in the same scope are not. + +### Storage representation is not the complete semantic domain + +When \`sizeof(E)\` is four, that alone does not mean every 32-bit pattern is a valid value of an enum without a fixed underlying type. The underlying integer *representation* and the enum's permitted *value range* are related but different concepts. Section 4 develops the distinction because it affects \`static_cast\`, sanitizer behavior, and wire-protocol parsing. --- -## 3. Underlying type and size rules +## 2. C++98 unscoped enums: convenient constants with implicit conversions -### C++98 +The C++98 form inherits much of C's original intent: a set of related named constants that can be passed to existing integer-oriented APIs. That convenience was essential for interoperability, but it also makes it easy to lose the intended domain. -* Underlying type is implementation-defined -* Must be large enough to represent all enumerators +### Enumerators enter the enclosing scope -### C++11+ +An unscoped enum introduces its enumerator names into the surrounding scope. The enum type itself can be named and used in declarations, but the enumerators are not protected from collisions with unrelated constants. -* You may explicitly specify the underlying type: +\`\`\`cpp +namespace graphics { + enum Color { red, green, blue }; + Color current = red; +} -```cpp -enum ErrorCode : unsigned short { A = 1, B = 2 }; -``` +// enum Other { red, yellow }; // conflicts if placed in graphics +\`\`\` -### Practical implications +Namespaces and class scopes reduce collision risk, but do not replace the stronger encapsulation of scoped enumerators. Modern code can still use unscoped enums deliberately when legacy C interfaces or integral conversion behavior is part of the contract. -* ABI stability requires explicit underlying types -* Network / file formats should never rely on default enum size -* `sizeof(enum)` is not guaranteed to equal `sizeof(int)` +### Integral promotion can change both type and arithmetic behavior ---- +An unscoped enum with a non-fixed underlying type promotes according to the integer promotion rules. It may become \`int\`, or another suitable integer type when its representable range does not fit. A fixed-underlying unscoped enum also permits conversion to its underlying type, with consequences for overload resolution. -## 4. Enum values outside enumerators +\`\`\`cpp +enum Position { left = 0, center = 1, right = 2 }; +static_assert(std::is_same_v); -* Casting an integer to an enum is allowed -* Resulting value may not correspond to any enumerator -* Merely *holding* such a value is not automatically UB -* Logic assuming "only valid enumerators exist" becomes broken +int adjusted = right + 10; +\`\`\` -```cpp -enum Mode { Idle, Run, Stop }; -Mode m = static_cast(99); // legal, but invalid state -``` +The result is an integer, not a newly constructed \`Position\`. This makes accidental arithmetic easy, and it is a reason to prefer scoped enums for states rather than numeric quantities. -Rule: -Validate when converting from external or untrusted data. +### The usual arithmetic conversions are not enum-preserving + +Once an unscoped enum enters a mixed arithmetic expression, the ordinary conversion machinery determines the result type. Signedness and integer rank can matter as much as the enum declaration itself. Treat warning-free arithmetic as insufficient evidence that an enum still denotes one of the application's defined states. + +\`\`\`cpp +enum Phase { initial = 1, final = 3 }; +auto next = final + 1; // integral result, not Phase +static_assert(std::is_integral_v); +\`\`\` + +If an algorithm genuinely needs modular arithmetic over a numeric domain, a plain integer or a deliberately designed wrapper may represent the requirement better than an enum. + +### Unscoped enumerators can still be qualified in modern C++ + +C++11 permits the \`E::member\` spelling for unscoped enumerators. Qualification improves readability and resolves ambiguities without transforming the enum into a scoped enum. + +\`\`\`cpp +enum LegacyResult { success, failure }; +auto result = LegacyResult::success; // C++11 +\`\`\` + +This is particularly useful while modernizing legacy code incrementally, because the type and implicit conversions remain unchanged. --- -## 5. Enums and constant expressions +## 3. C++11 scoped enumerations establish a stronger API boundary + +C++11 introduced \`enum class\` and \`enum struct\` as equivalent scoped-enumeration spellings. Their purpose is to prevent accidental name injection and implicit conversion to integer types. They do not, however, automatically validate external input. + +### Scoped enumerators stay inside the enum scope + +The distinction is immediately visible when two domains use the same common words. + +\`\`\`cpp +enum class Traffic { red, green }; +enum class Alert { red, green }; -* Enumerators are integral constant expressions -* Can be used in: +Traffic light = Traffic::green; +// int x = light; // ill-formed +\`\`\` - * `case` labels - * array bounds - * `static_assert` - * template non-type parameters +Scoped enums work well for state machines and typed options because an integer operation requires the programmer to explicitly cross the type boundary. -```cpp -enum Size { Small = 4, Large = 16 }; -int buffer[Large]; -``` +### The default underlying type of a scoped enum is \`int\` + +The declaration \`enum class Mode { idle, running };\` has \`int\` as its underlying type. It is not an implementation-selected arbitrary integer type. The underlying type may instead be specified explicitly, subject to the rules for an integral underlying type. + +\`\`\`cpp +#include +#include + +enum class Mode { idle, running }; +enum class WireMode : std::uint8_t { idle, running }; + +static_assert(std::is_same_v, int>); +static_assert(std::is_same_v< + std::underlying_type_t, std::uint8_t>); +\`\`\` + +An explicit fixed type is valuable when another ABI or representation contract requires particular width and signedness. A fixed underlying type alone does not define a portable byte-order serialization format. + +### Scoped types can have unnamed values + +\`enum class\` prevents implicit integer conversions, not explicit casts or construction from certain integer values. The type still has a range tied to its fixed underlying representation, and the range can be wider than the named enumerators. + +\`\`\`cpp +enum class Mode : std::uint8_t { idle = 0, active = 1 }; +Mode unknown = static_cast(200); // defined, but unnamed +\`\`\` + +A domain parser should decide whether \`200\` is rejected, reserved for forward compatibility, or mapped to an explicit unknown state. Type safety protects the interface from accidental mixing; it does not provide schema validation. --- -## 6. Scoped enums (`enum class`) - C++11+ +## 4. The value range and the underlying integer type are different contracts + +This is the most dangerous subtlety in classic enum code. A fixed-underlying enum and a non-fixed unscoped enum can respond differently to an integer cast—even when they appear to occupy identical storage on a particular platform. + +### Non-fixed unscoped enum ranges are based on enumerator values -### Motivation +For an unscoped enum without a fixed underlying type, the language defines a representable range based on the smallest bit-field width sufficient for its enumerators, not necessarily the full machine range of its storage type. The range need not be the closed interval between the smallest and largest named values. -Scoped enums fix most C++98 enum problems. +\`\`\`cpp +enum Permission { read = 1, write = 2, execute = 4 }; -### Properties +Permission combination = static_cast(3); // in range; unnamed +// auto invalid = static_cast(8); // undefined behavior +\`\`\` -* No implicit conversion to `int` -* Enumerators do not leak into enclosing scope -* Stronger type safety -* Underlying type can be fixed -* Forward declaration always allowed +A commonly observed implementation may retain the bits of \`8\` and appear to work, but the cast is not a portable operation. CWG 1766 clarified the out-of-range rule as undefined behavior; implementations and standards publications may describe its historical application differently. -```cpp -enum class Color : std::uint8_t { Red, Green, Blue }; -``` +### Fixed underlying types permit every representable underlying value + +When the underlying type is fixed, the enum's value range is that of the underlying type. Conversion from an integer uses the corresponding underlying-type conversion first. + +\`\`\`cpp +enum class Permission : std::uint8_t { + read = 1, write = 2, execute = 4 +}; + +auto unknown = static_cast(200); // defined unnamed value +\`\`\` + +This makes fixed-underlying enums suitable for preserving unknown protocol codes. It still does not make every such value *semantically valid* for an application. + +### An unnamed value is not inherently an invalid C++ object + +An enum object holding a representable value that lacks a name remains an enum object. That can be useful for bitmask combinations or extensible protocols. The correct classification has at least three layers: representable by the enum type; named by an enumerator; accepted by the application. + +\`\`\`cpp +enum class State : unsigned char { stopped = 0, started = 1 }; +State s = static_cast(2); // representable but unnamed + +bool known(State x) { + return x == State::stopped || x == State::started; +} +\`\`\` -```cpp -Color c = Color::Red; -// int x = c; // ERROR -``` +Conflating these layers is dangerous at trust boundaries. Validate integers *before* converting to a non-fixed enum if they may be outside its language-defined range. + +### Never use an out-of-range example as a harmless demonstration + +The existing \`01_enums_core/enums_core.cpp\` example casts \`20\` to an enum whose enumerators are only \`0\` and \`1\`. Because that enum has no fixed underlying type, it is not a valid demonstration of merely holding an unnamed enum value. It needs correction in the accompanying source example as well as in this chapter. + +The safe alternative is an in-range unnamed value on an enum whose range has room for one, or a fixed-underlying enum. A sanitizer need not diagnose every such invalid cast; correctness comes from the specified value-range contract. --- -## 7. Name scoping and collisions +## 5. Underlying types, layout, and ABI require explicit design + +An enum is a proper object type with alignment and size, and its layout can affect binary compatibility. This is one of the few places where spelling the underlying type is not merely stylistic: it is part of an interface specification. + +### An unscoped enum without a fixed underlying type leaves representation to the implementation + +The implementation chooses a compatible underlying integer type that can represent the enumerators, subject to the language rules. Relying on an observed \`sizeof\` in one build is not sufficient to design a stable library ABI. -### Unscoped enums +\`\`\`cpp +enum LegacyCode { ok = 0, error = 70000 }; +// sizeof(LegacyCode) is not a portable protocol width. +\`\`\` -```cpp -enum Color { Red, Green }; -enum Traffic { Red, Yellow }; // collision -``` +Compiler switches such as \`-fshort-enums\` can also affect layout decisions and break binary compatibility between compilation units. Such options belong in a documented whole-program ABI contract. -### Scoped enums +### Fixed underlying type implies matching size and representation -```cpp -enum class Color { Red, Green }; -enum class Traffic { Red, Yellow }; // OK -``` +A fixed-underlying enum has the same size, value representation, and alignment as its specified underlying type under the standard's enum rules. It does not guarantee the host machine's byte order or the ABI calling convention of a surrounding library. -### C++20: `using enum` +\`\`\`cpp +enum class PacketKind : std::uint16_t { data = 1, ack = 2 }; -```cpp -using enum Color; -Color c = Red; // shorter, but may reintroduce collisions -``` +static_assert(sizeof(PacketKind) == sizeof(std::uint16_t)); +static_assert(alignof(PacketKind) == alignof(std::uint16_t)); +\`\`\` -Guideline: -Use `using enum` sparingly and never in widely included headers. +The specific fixed-width integer typedef must exist for the target. \`std::uint16_t\` is not required on a machine without a suitable exact-width integer type. + +### Treat public ABI, persistence format, and source API as three separate contracts + +A public C++ function taking an enum has an ABI influenced by the target, toolchain, and compiler options. A stored protocol field has byte order and versioning requirements. A C++ source API has name lookup and conversion rules. Fixing an underlying type helps, but does not collapse these three independent questions into one guarantee. + +A robust protocol reader extracts a specified-width integer from bytes, validates or preserves it according to the schema, and then constructs the enum. Raw \`memcpy\` of a struct containing enums is not a platform-independent wire format. --- -## 8. Forward declaration rules +## 6. Forward declarations and opaque enum declarations are C++11 completeness tools + +Before C++11, arbitrary enum types could not be forward-declared in the way class types can. C++11 introduced opaque enumeration declarations when the underlying type is fixed. Their unusual property is that the type is already complete after the opaque declaration, even though the enumerators are not yet defined. + +### A scoped enum can be declared before listing enumerators -### Scoped enums +Because the default underlying type is \`int\`, this opaque declaration fixes enough representation information to form objects and apply \`sizeof\`. -```cpp -enum class ErrorCode; // OK -``` +\`\`\`cpp +enum class ErrorCode; +static_assert(sizeof(ErrorCode) == sizeof(int)); -### Unscoped enums (C++11+) +enum class ErrorCode { success, failure }; +\`\`\` -```cpp -enum Legacy : unsigned int; // must specify underlying type -``` +This is not analogous to a forward-declared incomplete class. Code cannot refer to \`ErrorCode::success\` before the definition, but the representation and completeness rules allow layout-sensitive declarations. -* Incomplete enum types can be: +### Unscoped opaque enums require an explicit underlying type - * declared - * used as pointers/references -* But not: +The C++11 declaration \`enum Legacy : unsigned;\` is permitted. Omitting its underlying type in an opaque unscoped declaration is not. - * `sizeof` - * value construction - * switching +\`\`\`cpp +enum Legacy : unsigned; +Legacy make_legacy(); // legal declaration + +enum Legacy : unsigned { zero = 0, one = 1 }; +\`\`\` + +Every subsequent definition must agree with the underlying type. This pattern can reduce header dependencies when enumerator names are needed in only a few implementation files. --- -## 9. Conversions and `std::underlying_type` +## 7. Conversions and overload resolution expose the limits of enum type safety -### Safe extraction of integer value +The boundary between enums and integers is asymmetric. Unscoped enums convert implicitly to integers, scoped enums generally require explicit conversion, and overload resolution can prefer an exact enum match over an otherwise convenient integer overload. -```cpp -template -constexpr auto to_underlying(E e) noexcept -{ - return static_cast>(e); -} -``` +### Exact enum matches outrank integral conversions + +An overload set can contain both enum-aware and integer-aware functions. Passing an enum selects the exact match, but removing that overload can silently cause legacy unscoped code to call the integral overload. + +\`\`\`cpp +enum LegacyMode { on, off }; + +void configure(LegacyMode); +void configure(int); + +void use() { configure(on); } // chooses configure(LegacyMode) +\`\`\` + +The risk is not that enums are aliases for integers; the risk is that implicit conversion makes an unintended fallback viable. -* Essential for: +### Scoped enums do not participate in implicit arithmetic - * logging - * serialization - * bitmask operations - * interop with C APIs +Unlike \`LegacyMode\`, a scoped \`enum class\` does not implicitly select an \`int\` overload and does not accept \`operator+\` from the built-in arithmetic rules. + +\`\`\`cpp +enum class Mode { on, off }; +// auto n = Mode::on + 1; // ill-formed +// configure(Mode::on); // ill-formed without a suitable overload +\`\`\` + +A named explicit conversion is clearer than a C-style cast because it makes the crossing of abstraction boundaries visible in code review. + +### C++17 direct-list initialization is intentionally narrow + +C++17 permits direct-list initialization of a scoped enum, and of an unscoped enum with a fixed underlying type, from an integral value when the initialization rules permit it. This is not implicit conversion and is not a license for narrowing. + +\`\`\`cpp +enum class ByteKind : unsigned char {}; +ByteKind one{1}; // C++17 +// ByteKind too_large{300}; // narrowing: ill-formed +\`\`\` + +The rule supports small strong integer wrappers and selected low-level APIs. A named enumerator or parsing function is still preferable when an application requires a closed domain. + +### C++23 \`std::to_underlying\` names the intended conversion + +Before C++23, code typically used \`static_cast>(value)\` or a short helper. The standard library now provides \`std::to_underlying\` in \`\`. + +\`\`\`cpp +#include +#include + +enum class Code : unsigned { ok = 0, error = 17 }; + +constexpr auto raw(Code code) noexcept { + return static_cast>(code); // C++11+ +} +#if defined(__cpp_lib_to_underlying) && __cpp_lib_to_underlying >= 202102L +static_assert(std::to_underlying(Code::error) == 17u); +#endif +\`\`\` + +It extracts the representation value; it does not validate whether the enum is one of the named values. --- -## 10. Enums as bitmasks +## 8. Bitmask enums require a different domain model from state enums + +A state enum usually denotes *one of several mutually exclusive states*. A bitmask enum usually denotes *any allowed combination* of independent flags. If both are modeled by \`enum class\`, their common syntax can conceal a major semantic difference. -### Why scoped enums are better for flags +### Choose non-overlapping bits and an unsigned underlying type -* No accidental arithmetic -* No implicit conversion -* Explicit operator definitions required +A permission flag set can use individual bits, usually with an explicitly unsigned fixed representation. A name such as \`all\` should mean exactly the OR of supported flags, not simply all bits of the storage type. -```cpp -enum class Perm : uint32_t { - Read = 1 << 0, - Write = 1 << 1 +\`\`\`cpp +enum class Permission : std::uint32_t { + none = 0, + read = 1u << 0, + write = 1u << 1, + execute = 1u << 2, + all = (1u << 0) | (1u << 1) | (1u << 2) }; -``` +\`\`\` + +An unsigned representation makes shifts and mask operations easier to reason about. Always ensure the shift count is less than the underlying width and that the left operand has a suitable unsigned type. + +### Constrain bitwise operators to the intended enum -### Operator overloads required +Do not add generic bitwise operators to every enum; doing so makes nonsensical operations such as \`Traffic::red | Traffic::green\` compile. A focused overload keeps permissions combinable without weakening unrelated state types. -```cpp -Perm operator|(Perm a, Perm b); -Perm operator&(Perm a, Perm b); -``` +\`\`\`cpp +constexpr Permission operator|(Permission a, Permission b) noexcept { + using U = std::underlying_type_t; + return static_cast( + static_cast(a) | static_cast(b)); +} +\`\`\` + +A reusable flag-operator library can use an opt-in trait or concept to constrain overloads, but it must still define the legal mask and avoid unexpected argument-dependent lookup behavior. + +### A raw bitwise complement usually sets unwanted bits -### Pitfalls +The complement of the numeric underlying value flips all bits, including bits with no named flag. This is not a type error because the fixed underlying enum can represent them, but it may violate the application-level mask domain. + +\`\`\`cpp +constexpr Permission without_read(Permission p) noexcept { + using U = std::underlying_type_t; + constexpr U valid = static_cast(Permission::all); + return static_cast( + static_cast(p) & (valid ^ static_cast(Permission::read))); +} +\`\`\` -* Prefer unsigned underlying type -* `~flag` flips *all bits*, not just known flags -* Often need an `All` mask: +The example removes a known bit without introducing unknown ones. When parsing flags from a protocol, validate \`(raw & ~valid_mask) == 0\` or define a deliberate rule for forward-compatible extensions. -```cpp -(~x) & All -``` +### Testing for any flags is different from testing for all flags + +A common helper returns true if *any* requested bit is present. Applications often also need a separate helper that tests whether *every* requested bit is present. + +\`\`\`cpp +constexpr bool contains_all(Permission actual, Permission required) { + using U = std::underlying_type_t; + U a = static_cast(actual), r = static_cast(required); + return (a & r) == r; +} +\`\`\` + +The zero-mask case follows naturally: every set contains all bits of \`none\`. Whether that is desirable must be settled by the API contract. --- -## 11. Enums in `switch` statements +## 9. Switch exhaustiveness is a diagnostic policy, not a language guarantee + +A \`switch\` on an enum is expressive precisely because the compiler can know the set of *named* enumerators. The compiler does not, however, guarantee that the switch handles every representable enum value. The best design separates missing named cases from intentionally unknown input. + +### The absence of a default may reveal newly added enumerators -### No enforced exhaustiveness +For internal closed state machines, a switch with explicit cases and an unreachable fallback after the switch can allow \`-Wswitch\` to warn when a new enumerator appears. A \`default\` branch can hide that diagnostic with some warning modes. -* C++ does not require handling all enumerators -* Compilers may warn, but language does not guarantee safety +\`\`\`cpp +enum class Mode { idle, running, stopped }; -```cpp -switch (mode) { - case Mode::Run: ... +const char* name(Mode m) { + switch (m) { + case Mode::idle: return "idle"; + case Mode::running: return "running"; + case Mode::stopped: return "stopped"; + } + return ""; } -``` +\`\`\` -### Invalid enum values +The final return is still essential because a fixed-underlying enum can hold an unnamed value. For genuinely unreachable internal states, fail explicitly instead of returning a plausible but incorrect state. -* `switch` may reach no `case` -* Always handle default or validate beforehand +### A wire decoder should validate integers before dispatch -```cpp -default: return ""; -``` +Do not make a \`switch\` over an unchecked cast the sole source of validation. Parse the incoming integer and map only accepted codes to enum values, so the distinction between known, reserved, and invalid inputs remains visible. ---- +\`\`\`cpp +#include -## 12. Enums and overload resolution +std::optional decode(unsigned raw) { + switch (raw) { + case 0: return Mode::idle; + case 1: return Mode::running; + case 2: return Mode::stopped; + default: return std::nullopt; + } +} +\`\`\` -### Unscoped enums +Using \`std::optional\` is a C++17 option. C++98 code can express the same contract through a boolean return and output parameter. Explicit parsing is more stable than assuming every integer cast produces a meaningful enumerator. -* Implicit conversion to `int` participates in overload resolution -* May select unintended overload +### C++20 \`using enum\` is a local convenience with lookup consequences -### Scoped enums +C++20 \`using enum\` makes enumerators available in the current scope, including scoped-enum members. This can make a short switch or local dispatch table readable, but it can also recreate the name-collision risks avoided by scoped enums. -* No implicit conversions -* Safer and more predictable overload behavior +\`\`\`cpp +void handle(Mode m) { + using enum Mode; + switch (m) { + case idle: break; + case running: break; + case stopped: break; + } +} +\`\`\` + +Prefer short local scopes. Avoid importing common enumerator names into public headers or broadly shared namespaces. --- -## 13. Enums and ABI / interfaces +## 10. Template metaprogramming and generic enum constraints changed incrementally -### Public headers should: +An enum's type-level identity can be used to select overloads, control serialization, and write reusable formatting helpers. Each standard added vocabulary that reduces the need for handwritten detection code, but it remains easy to confuse a type trait with actual enumerator introspection. -* Use `enum class` -* Fix underlying type -* Avoid unscoped enums -* Avoid relying on `sizeof(enum)` +### \`std::underlying_type\` exposes representation types, not enumerator lists -### Serialization / networking +C++11 \`std::underlying_type\` and C++14 \`std::underlying_type_t\` are useful for converting enum values to the chosen underlying integer representation. -* Always serialize underlying value -* Never rely on enumerator ordering implicitly +\`\`\`cpp +template +constexpr auto representation(E e) noexcept + -> std::underlying_type_t +{ + return static_cast>(e); +} +\`\`\` ---- +The trait does not reveal the number of enumerators, their identifiers, or gaps in their values. A general-purpose enumeration iteration library cannot be implemented from \`underlying_type\` alone. + +### C++20 concepts can restrict templates to enumeration types + +Generic formatting or serialization functions often need a deliberate constraint to avoid accepting ordinary integers by accident. \`std::is_enum_v\` has existed since C++17, and C++20 concepts make a constraint visible at an interface. + +\`\`\`cpp +template +concept Enumeration = std::is_enum_v; + +template +constexpr auto enum_bits(E e) { + return static_cast>(e); +} +\`\`\` + +The concept here is user-defined; C++20 does not provide a built-in \`std::enum_type\` concept. A constraint says what types are admitted, not whether an enum is intended as a flag set. -## 14. Enums vs other integer-like types +### C++23 \`std::is_scoped_enum\` separates the two language categories -| Type | Type Safety | Implicit Conversion | Scope | Use Case | -| --------------- | ----------- | ------------------- | -------- | -------------- | -| `int` | none | yes | global | raw arithmetic | -| unscoped `enum` | weak | yes | injected | legacy C APIs | -| `enum class` | strong | no | scoped | modern C++ | -| `std::byte` | strong | no | scoped | raw memory | +C++23 adds \`std::is_scoped_enum\`, which allows generic code to distinguish classic unscoped enums from scoped enums without guessing from convertibility to \`int\`. + +\`\`\`cpp +#if defined(__cpp_lib_is_scoped_enum) && __cpp_lib_is_scoped_enum >= 202011L +static_assert(std::is_scoped_enum_v); +static_assert(!std::is_scoped_enum_v); +#endif +\`\`\` + +This is useful for modernizing generic APIs and applying different policies to legacy integer-compatible enums. It still does not tell whether a particular scoped enum is a bitmask. --- -## 15. Common anti-patterns +## 11. Enum design must distinguish validation, versioning, and serialization + +Enums appear frequently in protocol fields, configuration files, persisted states, and plugin APIs. Their representation may be small, but their compatibility problems are not. This is an area where a few lines of explicit parsing often prevent expensive production failures. + +### Persist stable numeric identifiers rather than declaration positions + +Implicit numbering is convenient until someone inserts a new enumerator in the middle. Any protocol, file format, or external API should assign stable numeric IDs deliberately and document reserved ranges. + +\`\`\`cpp +enum class Message : std::uint16_t { + hello = 1, + payload = 10, + goodbye = 100 +}; +\`\`\` + +Never treat enum ordinal position or contiguous naming as a portable serialization scheme. Aliases can even share the same number, defeating one-to-one inverse mappings. + +### Decide whether unknown values are errors or forward-compatible extensions + +A strict protocol might reject every unrecognized code. A rolling-upgrade system may need to preserve unknown fixed-underlying codes so newer messages can pass through older nodes without data loss. + +These are different schemas. For strict domains, parse to a known enumerator or return an error. For extensible domains, use a representation that can retain unknown numeric values and distinguish them from known commands during dispatch. -* ❌ Using unscoped enums in headers -* ❌ Relying on default underlying type -* ❌ Assuming enum values are contiguous -* ❌ Using enums as bitmasks without operators -* ❌ Switching without handling invalid states +### C interop and C++ enum class are different source-level interfaces + +C headers usually use C-compatible enum declarations or integers. A C++ scoped enum is not a drop-in C type, even if its representation has the same width. Extern "C" adjusts linkage rules, not C++ type compatibility or calling convention details automatically. + +For a stable C ABI, expose the exact documented scalar representation and convert at the language boundary. Avoid exporting an implementation-selected classic enum type as though its storage and ABI could never change. + +### Concurrency safety does not follow from using enums + +An enum can be a natural state stored in \`std::atomic\` when the type meets the required conditions, but atomic reads and writes do not guarantee that transitions obey a protocol. + +\`\`\`cpp +#include + +enum class State : unsigned char { idle, active, stopped }; +std::atomic state{State::idle}; +\`\`\` + +The atomic prevents data races on that object under valid use; it does not enforce legal transitions such as prohibiting \`stopped -> active\`. Use compare-exchange loops or a synchronized state machine when transition invariants matter. --- + +## 12. C++26 static reflection changes enum introspection fundamentally + +Before standard reflection, enumerator-name utilities typically relied on maintained tables, macros, generated code, or implementation tricks. These approaches can drift when enumerators are added, especially where enum values contain gaps or duplicates. C++26 reflection adds a standard compile-time mechanism for inspecting an enum's enumerators, but this requires actual compiler and library support. + +### Reflection exposes enumerators rather than guessed integer ranges + +C++26 reflection introduces \`\` and \`std::meta::enumerators_of\` to obtain reflections of the enumerators of an enum type in declaration order. This is categorically different from scanning all possible integral values or inferring names from vendor function-signature strings. + +The following is **C++26 reflection syntax**, not C++20-compatible code: + +\`\`\`cpp +// C++26 only, when reflection and are supported: +#include +#include + +enum class Fruit { apple = 1, pear = 8, plum = 8 }; +consteval auto entries() { + return std::meta::enumerators_of(^^Fruit); +} +\`\`\` + +The result describes *declarations*, which means aliases with equal numeric values may both appear. Applications must still choose whether a value-to-name conversion prefers the first declared alias, all aliases, or a canonical one. + +### Name extraction and value extraction have separate semantics + +A reflection of an enumerator can be used with the appropriate \`std::meta\` facilities to obtain identifier spelling and to generate a value-producing expression. This makes it possible to build compile-time tables without writing the same enumerator list twice. + +Reflection is not a free runtime registry: it is a compile-time facility used by code-generation and constant-evaluation mechanisms. The generated tables and strings still consume program resources and may require design choices about linkage and binary size. + +### Compiler and standard-library support must be feature-tested + +C++26 reflection has a feature-test macro, and the library's \`\` interface must actually be available. The spelling and implemented subset of these facilities can evolve as toolchains adopt the standard. + +Keep reflection examples in C++26-only blocks or separate demonstration sources. For the C++20 baseline of this repository, retain explicit tables or a documented external enum-reflection library, and avoid claiming that old C++ compilers can enumerate identifiers portably. + +### Reflection does not replace domain validation + +A reflection-generated list tells you which enumerators are declared; it does not automatically decide whether unnamed fixed-underlying values are supported extension codes. Nor does it prove that a bitmask combination is acceptable. + +The same three-layer model—representable, named, accepted by the application—continues to govern enumeration correctness in C++26. + --- -## Rules worth keeping in working memory +## 13. Cross-cutting traps worth recognizing in production code -The central distinction in enumerations and their underlying representations is between what the C++ type system guarantees, what the implementation commonly does, and what an API merely assumes. Carrying that distinction from declarations through operations avoids most of the surprises discussed above. +The traps here have a common source: representing one concept as an integer does not mean every integer operation makes sense in that concept's domain. -> A valid low-level operation needs a language-level contract, not just a machine-level outcome that looks plausible. +### A cast can fail before the program reaches its validator -### Core rules +If an untrusted number is cast to a non-fixed unscoped enum outside its value range, the operation itself can have undefined behavior. Validating the resulting enum afterward is too late. Parse and validate the integer first, or choose a fixed-underlying representation explicitly when unnamed values must be preserved. + +### A fixed underlying type does not guarantee named values -The following rules condense the chapter into reviewable decisions: +\`enum class Mode : std::uint8_t\` can hold unnamed values such as \`200\`. Robust switching, logging, and serialization must have a policy for these cases, even when every named enumerator is handled. -1. **Prefer scoped enums when accidental integer conversion would be unsafe.** -2. **Specify an underlying type when layout or ABI demands it.** -3. **Do not assume every underlying value names an enumerator.** -4. **Implement flag operators deliberately rather than treating every enum as a bitmask.** +### A flags type is not necessarily a state type +It is sensible to combine independent flags with bitwise OR; it is usually nonsensical to OR two mutually exclusive states. Do not use one generic operator-enabling technique across all enumerations. -The practical guidance already emphasized in this chapter remains applicable: +### A default switch case trades exhaustiveness diagnostics for fallback behavior -Use `enum class` with explicit underlying types by default. -Only use unscoped enums when: +A \`default\` branch may conceal newly added enumerators from warning configurations, while omitting a fallback can make unnamed values flow past the switch. Use explicit named cases and a deliberate fallback policy chosen for the API. -* required for C compatibility -* interacting with legacy APIs -* performance constraints mandate it (rare) +### The underlying representation is not a schema -Enums are a type-safety tool, not just named integers - treat them as such. +Stable protocols require explicit numeric identifiers, conversion rules, endianness, and versioning. A fixed-width enum is one useful component of that design, not the entire design. --- +## Rules worth keeping in working memory + +The most durable model is to distinguish the enum *type*, its underlying *representation*, the language-defined *range*, its *named enumerators*, and the application's *accepted domain*. A strong API keeps those layers separate instead of assuming they are identical. + +> An enum is a semantic type implemented using integers; the integers alone do not define its valid states. + +### Core rules + +1. **Prefer scoped enums for independent semantic domains.** Their lack of implicit integer conversion avoids accidental arithmetic and overload selection. +2. **Fix the underlying type when representation is a contract.** Also document serialization and ABI requirements separately. +3. **Validate before casting to a non-fixed enum.** Out-of-range conversion can be undefined behavior. +4. **Never equate an unnamed enum value with undefined behavior.** Fixed-underlying enums can legally represent unnamed values. +5. **Distinguish state enums from flag enums.** Only the latter should receive opt-in bitwise operators. +6. **Preserve exhaustiveness diagnostics without forgetting unknown inputs.** Cover named cases and define an explicit fallback. +7. **Treat reflection as introspection, not automatic business-rule validation.** C++26 enumerator lists still need domain semantics. + ### Pitfalls at a glance -These recurring failures are particularly useful to recognize in code review because each starts from a plausible but insufficient assumption. +Each failure below is a confusion between two layers of the enum model. | Pitfall | What happens | Do instead | |---|---|---| -| `static_cast(wire_value)` assumed validated | May produce unnamed enumerator value | Validate protocol values | -| Implicit unscoped enum to integer | Can select unintended overloads | Prefer `enum class` | -| Unspecified enum storage expected stable | ABI/layout coupling | Specify underlying type at interfaces | - -Each safer alternative makes an implicit precondition visible either in the type or in the code that checks it. +| \`static_cast(20)\` where non-fixed range excludes 20 | Undefined behavior | Validate source before cast; use a fixed type where appropriate | +| Assume \`enum class E\` has compiler-chosen underlying type | Contradicts C++11 default of \`int\` | Document default or explicitly select an integral underlying type | +| Assume every enum value has a declared name | Unknown or combined values remain possible | Provide validated parsing and fallback formatting | +| Use \`~flag\` without a mask | Produces bits outside supported flags | Mask to the declared flag universe | +| Treat a fixed-width enum as a wire layout | Byte order and compatibility remain unspecified | Serialize a specified-width integer explicitly | +| Use a default switch branch to silence warnings | Missing newly added enumerators can be overlooked | Enable enum warnings and apply a deliberate fallback strategy | ### Mental model summary -The underlying distinction is consistent across the subject: a language guarantee is portable; an implementation choice must be checked; a domain requirement must be stated by the program. - -| Question | Language guarantee | Implementation detail | Application responsibility | +| Question | Non-fixed unscoped enum | Fixed unscoped enum | Scoped enum | |---|---|---|---| -| What may this operation assume? | Specified preconditions and types | ABI, representation, optimizer strategy | Select the appropriate contract | -| What happens at an edge case? | Specified result or behavior category | Diagnostics and machine reaction | Validate inputs and lifetimes | -| Can this cross an interface? | Only what types and linkage guarantee | Toolchain and platform compatibility | Document conversions and ownership | +| Enumerators injected into enclosing scope | Yes | Yes | No | +| Implicit conversion to integers | Yes | Yes | No | +| Underlying type predetermined by declaration | No | Yes | Yes (\`int\` by default) | +| May legally hold unnamed values | Yes, within enum range | Yes, within underlying range | Yes, within underlying range | +| Appropriate for flag combinations | Sometimes, with care | Yes, with a flag contract | Yes, with explicit operators | +| Can be opaquely declared since C++11 | No (without fixing type) | Yes | Yes | -The third column cannot substitute for the first, and the fourth is where domain-specific requirements belong. +The key difference is not just whether conversion is implicit: the permitted value range can change the definedness of the conversion itself. ### Review checklist -Use this checklist when changing enumerations and their underlying representations code or reviewing low-level interfaces: +When reviewing enum-heavy code, ask: -- [ ] Have we distinguished language guarantees from platform-specific observations? -- [ ] Are all input domains and conversion or lifetime preconditions explicit? -- [ ] Can the relevant edge cases be tested without executing undefined behavior? -- [ ] Does the chosen API encode as much of the intended contract as practical? +- [ ] Are all externally supplied numbers validated before any potentially out-of-range conversion? +- [ ] Is the underlying type fixed and documented wherever the ABI or protocol depends on it? +- [ ] Does every enum have an explicit policy for unknown or reserved values? +- [ ] Are bitwise operators restricted to flag types and masks restricted to supported bits? +- [ ] Can adding an enumerator trigger diagnostics at all relevant switches? +- [ ] Are enum identifiers, raw values, and protocol encodings intentionally stable? +- [ ] Are library-dependent C++23/26 features feature-tested rather than assumed? --- ## Diagnostics, useful compiler settings and extensions -Compilers can diagnose many suspicious uses of enumerations and their underlying representations, but they cannot infer every application invariant. A clean warning build is useful evidence, not proof: tools can recognize some invalid expressions statically and others only when particular paths execute. +Compiler diagnostics can detect incomplete switches and accidental enum/integer conversions, but they cannot infer an application's protocol policy. A warning-clean build does not prove that casting arbitrary wire values to an enum is safe or that every named value represents a supported runtime state. ### Warnings -Start with high-signal diagnostics and add specialized checks where the chapter's failure modes justify them. - -| Compiler | Flags | What they reveal | +| Compiler | Settings | Useful coverage | |---|---|---| -| GCC | `-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion` | Suspicious conversions, extensions, and common type mistakes | -| Clang | `-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion` | Similar mistakes, with different analysis coverage | -| MSVC | `/W4 /permissive-` | Common warnings and nonconforming language extensions | +| GCC | \`-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wconversion -Wenum-conversion\` | Missing switch enumerators, suspicious conversions and extension use | +| Clang | \`-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wenum-conversion\` | Incomplete switches and enum conversions, with toolchain-specific extra enum warnings | +| MSVC | \`/W4 /permissive- /analyze\` | Common enum switch and conversion diagnostics; consider C4061/C4062 configurations | -### Sanitizers and runtime checks +\`-Wswitch-enum\` can reveal omitted named cases even when a \`default\` branch exists, subject to compiler behavior. It cannot prove that every representable unnamed value is handled. -Runtime instrumentation can expose executed failures but does not validate every semantic assumption. +### Sanitizers and runtime checks -| Tool | Setting | Best use | +| Tool | Setting | What it contributes | |---|---|---| -| AddressSanitizer | `-fsanitize=address` or MSVC `/fsanitize=address` | Invalid object lifetime and memory access when applicable | -| UndefinedBehaviorSanitizer | `-fsanitize=undefined` (GCC/Clang) | Instrumented undefined operations; coverage is incomplete | -| clang-tidy / static analysis | Selected checks and `/analyze` (MSVC) | Suspicious contracts, conversions, and API use | +| Clang UBSan enum check | \`-fsanitize=enum\` | Detects some loads of enum representations outside a valid range; no business-domain validation | +| Clang/GCC UBSan | \`-fsanitize=undefined\` | Selected undefined behavior paths, with varying instrumentation and limitations | +| Static analysis | clang-tidy, cppcheck, MSVC \`/analyze\` | Suspicious casts, missing cases, and enum/integer API assumptions | +| Protocol tests | Exhaustively test allowed codes, reserved codes, and malformed inputs | Enforces the application-defined accepted domain | -### Semantics-changing options +A sanitizer may not catch the creation of an invalid enum value at the exact cast site. Do not use runtime instrumentation as an excuse to execute knowingly undefined conversions in example programs. -Language mode, optimizer assumptions, and vendor extensions can affect which source expressions are accepted and how they are interpreted. The following controls deserve deliberate treatment rather than being regarded as speed settings alone. +### Semantics-changing options | Option | Effect | Recommendation | |---|---|---| -| `-std=gnu++20` vs `-std=c++20` | GNU mode may accept non-standard constructs | Prefer ISO mode when teaching portable rules | -| `-fno-strict-aliasing` | Weakens some alias-based optimization assumptions, not object-lifetime rules | Never use it to justify invalid accesses | -| `-fwrapv` (GCC/Clang) | Gives signed overflow wrapping semantics under that compiler option | Use only under a documented toolchain contract | -| `-ffast-math` (GCC/Clang) | Permits FP transformations inconsistent with strict numerical assumptions | Avoid for reproducibility and FP environment examples | +| GCC/Clang \`-fshort-enums\` | Can change non-fixed enum representation and ABI | Avoid across independently built binary interfaces | +| \`-std=gnu++20\` versus \`-std=c++20\` | GNU extensions can change which declarations are accepted | Use ISO language modes for portable examples | +| Clang/GCC optimization choices | Can expose reliance on undefined out-of-range enum values | Correct invalid conversions, rather than relying on \`-O0\` observations | +| MSVC \`/permissive-\` | Tightens conformance checking | Use for portable language contracts | -These options affect different topics to different degrees; only enable a topic-specific workaround when it addresses a known and measured requirement. +These options affect source acceptance and ABI assumptions, not just speed. Review build flags whenever binaries exchange enum-containing values. ### Compiler extensions -Vendor builtins can improve diagnostics or performance, but they are not portable substitutes for a standard language rule. - -| Extension | Compilers | Use | Portable approach | +| Extension | Toolchains | Purpose | Portable alternative | |---|---|---|---| -| `__builtin_assume` / `__assume` | Clang / MSVC | Gives optimizer an unchecked precondition | Validate invariants; use `[[assume]]` only where C++23 is supported | -| `__builtin_object_size` | GCC/Clang | Estimates available object storage | Carry an explicit extent in APIs | -| `__attribute__` / `__declspec` attributes | GCC/Clang / MSVC | Vendor-specific linkage/layout hints | Prefer standard attributes where applicable | +| \`__PRETTY_FUNCTION__\`-based enum-name extraction | GCC/Clang and similar compiler-specific signatures | Attempts to discover names by instantiating templates | Explicit tables or C++26 reflection where supported | +| \`__FUNCSIG__\`-based extraction | MSVC | Similar compiler-specific name discovery | Explicit tables or C++26 reflection | +| Vendor flag-enum attributes | Selected compiler/toolchain environments | Diagnose intended bitmask use | Explicit bitwise operators and documented masks | +| \`-fshort-enums\` | GCC/Clang | Narrow implementation-selected representations | Explicit fixed underlying types at public interfaces | -Keep extensions behind small wrappers, prefer `__cpp_*`/feature tests over compiler versions, and use `-pedantic-errors` or `/permissive-` when checking standard conformance. +A helper that depends on parsing compiler-provided function-signature strings is not standard enum reflection; isolate it, test exact supported compilers, and never treat it as a portable language guarantee. ### Libraries and tooling beyond the standard -The C++ Core Guidelines and the static analyzers built around them can help express and review preconditions that a raw type cannot encode. - -| Tool | Purpose | When useful | +| Library/tool | Purpose | When useful | |---|---|---| -| clang-tidy / cppcheck | Static analysis | Reviewing conversion, lifetime, and interface contracts | -| Microsoft GSL | Non-owning and bounds-oriented vocabulary | Modernizing raw-pointer/size interfaces | -| Compiler Explorer | Compare generated code | Verifying performance assumptions against actual toolchains | +| magic_enum (third party) | Convenient enum names and lookup through compiler-dependent mechanisms | C++17/20 code when tradeoffs and supported enum ranges are understood | +| Better Enums / generated tables | Stable pre-reflection metadata | When the enumerator list must be reused for validation/formatting | +| clang-tidy | Static analysis | Review switch completeness and suspicious numeric conversions | +| Compiler Explorer | Compare compiler diagnostics and ABI effects | Investigate promotions and fixed-underlying representation | -The best defense is layered: expressive types and APIs first, then warnings, targeted tests, runtime checking, and narrowly isolated extensions. +Prefer explicit domain contracts, layer warnings and sanitizers on top, and keep non-standard introspection behind well-tested interfaces. --- ## Standards timeline -The core model of enumerations and their underlying representations evolved incrementally; newer standards add safer vocabulary without retroactively rewriting all legacy expressions. The milestones below separate changes to the language from changes to available library interfaces. +Enum evolution is an incremental move from integer-compatible named constants toward scoped domain types, explicit representation, and finally standardized compile-time introspection. -### C++98/03: The original model +### C++98/03: Unscoped integer-compatible enumerations -Unscoped enumerations and underlying representations are long-standing. +Classic enums provide named integral constants with implicit integer conversions and implementation-selected underlying representations. Their values and promotions participate in integer expression rules; the language does not supply built-in enumeration names or iteration. -### C++11: Stronger types and interfaces +### C++11: Scoped enums and fixed underlying representations -Scoped enums and fixed underlying types substantially improve APIs. +\`enum class\`/\`enum struct\`, explicit underlying types on unscoped enums, opaque declarations, and \`std::underlying_type\` provide stronger type-safety and layout control. An opaque declaration with fixed underlying type yields a complete enum type even before the enumerator list appears. -### C++14: Incremental refinement +### C++14: Alias convenience for underlying-type traits -No material change to the underlying scalar model. +\`std::underlying_type_t\` reduces template verbosity; no fundamental change to the enum value model occurs. Existing distinction between representation and semantic domain remains. -### C++17: Library and deduction evolution +### C++17: Direct-list initialization from integers -No material change to enum validation obligations. +Single-element direct-list initialization can initialize suitable enums from integers under constrained rules, without creating a general implicit integer conversion. \`std::is_enum_v\` is a convenient variable-template spelling for generic code. -### C++20: Modern vocabulary +### C++20: Scoped enumerator imports and generic constraints -Concepts can express enum constraints in templates. +\`using enum\` introduces enumerator names into a selected scope; concepts make it easier to constrain enum-generic APIs. The old implicit-conversion rules for unscoped enums and the separate notion of unnamed values still apply. -### C++23: Further standard facilities +### C++23: Standard underlying-value extraction and scoped-enum traits -`std::to_underlying` exposes the underlying value explicitly. +\`std::to_underlying\` in \`\` provides a named integer conversion, while \`std::is_scoped_enum\` identifies scoped enumeration types in generic code. Neither offers general enumerator-list introspection. -### C++26: Emerging improvements +### C++26: Static reflection and enumerator discovery -Reflection-related conveniences remain an evolving area; do not assume automatic enum iteration. +C++26 static reflection introduces \`\` and \`std::meta::enumerators_of\`, permitting compile-time access to declared enumerators and supporting generation of mapping utilities. This is a genuine advance beyond hand-maintained tables, although real availability requires compiler and standard-library support and reflection does not validate business-domain values. -When documenting a facility, state its required language/library version rather than inferring support from the compiler's branding. +When discussing support, name the precise standard and the feature-test conditions used by the example instead of assuming every compiler labeled C++26 has the full library. --- ## Migration note for Java / Python / C# developers -Readers familiar with managed runtimes often expect enumerations and their underlying representations to come with runtime metadata, automatic lifetime management, or checked failures. In C++, some of those services come from a chosen library type, while raw language mechanisms may deliberately expose more responsibility to the caller. The important translation is from a *runtime guarantee* in one language to a *type, contract, and lifetime guarantee* in C++. +Languages with managed enum-like classes often provide runtime access to named members and convenience APIs for strings, membership, or reflection. C++ enums historically supply an efficient typed integer domain, not an automatically maintained runtime registry. Modern scoped enums improve type safety, but do not by themselves enforce a closed set of named values. ### Where intuition transfers and where it breaks -The same apparent operation may have a different failure mode or storage model in each language. - | Concept | Java | Python | C# | C++ reality | |---|---|---|---|---| -| enum | named constant group | Enum is a class with instances | `Enum`/`IntEnum` classes | Enum with value metadata | -| Invalid access/operation | Usually throws or is checked | Usually raises an exception | Often throws in safe code | May be ill-formed, defined, unspecified, or undefined depending on the operation | -| Lifetime | Managed object reachability | Reference counting / GC | Managed GC | Automatic, dynamic, and explicitly borrowed lifetimes coexist | - -The distinction matters at API boundaries: a familiar surface syntax does not imply familiar failure behavior. +| Enumeration model | Class-based enum constants | \`Enum\` / \`IntEnum\` objects | Value-type \`enum\` with fixed underlying integer | Distinct enum type with scoped or unscoped enumerators | +| Unnamed integer value | Not generally a Java enum instance | \`Enum\` construction typically rejects unknown values | Explicit casts can produce unnamed values | Fixed-underlying enum can represent unnamed values | +| Listing members | \`values()\`/reflection | Iteration and \`__members__\` | \`Enum.GetValues\` | No portable built-in listing before C++26 reflection | +| Integer conversion | Explicit numeric fields by design | \`IntEnum\` integrates with int | Explicit enum/numeric conversion | Implicit for unscoped; explicit for scoped | +| Invalid range | Managed validation and exceptions | Exceptions for unrecognized \`Enum\` values | Underlying integral value can be cast | Out-of-range conversion to non-fixed enums may have UB | ### Common wrong assumptions -One tempting assumption is that a successful local test demonstrates that an operation is safe. For the low-level C++ rules in this chapter, a test can demonstrate behavior of one build, but cannot establish portability or rule out undefined behavior. Another is that an object is kept alive by every handle referring to it; non-owning pointers, references, and views do not do that. Finally, a managed-language exception should not be presumed to exist at an equivalent C++ failure point. +An enum class in Java behaves like a closed collection of singleton objects; a C++ \`enum class\` does not guarantee that every representable value has a named enumerator. A Python \`Enum\` constructor often raises for an unknown integer; the corresponding C++ cast may produce an unnamed value or even undefined behavior for a non-fixed enum. C# programmers will recognize unnamed enum values but must still account for C++'s distinct range rules and ABI. ### Idiomatic C++ replacement -The following mappings help make intent explicit without imitating a managed runtime mechanically. - -| Habit from managed languages | Idiomatic C++ | +| Managed-language habit | Idiomatic C++ | |---|---| -| Closed domain | Use `enum class` with explicit parsing/validation | -| Flags | Define underlying bit operations and masks intentionally | -| Expect automatic runtime bounds/lifetime checks | Select an owning container or checked API; validate preconditions explicitly | +| Query names/values automatically | Maintain a constexpr table, use a tested library, or C++26 reflection | +| Rely on constructor rejection of unknown codes | Parse integers explicitly and return \`std::optional\` or an error result | +| Combine flags through general integer operators | Define constrained typed flag operations and a valid mask | +| Persist ordinal positions | Use explicit stable underlying values and a defined byte serialization | +| Depend on exhaustive enum dispatch | Use warning-backed switches with a deliberate unknown-value fallback | -Use these choices because they express the program's requirements, not merely because they resemble familiar constructs from another language. +The important adaptation is to encode the intended *domain* in APIs rather than expecting the runtime to infer it from enumerator spelling. --- ## Further reading -These references document the underlying language rules and library contracts. They are starting points for checking precise preconditions; the chapter's examples explain how the rules interact. +These references are useful for checking standard wording on ranges, conversions, enum declarations, and reflection; the worked examples above show how the pieces interact. ### Standard and language reference -* [language/enum](https://en.cppreference.com/w/cpp/language/enum) -* [types/underlying_type](https://en.cppreference.com/w/cpp/types/underlying_type) -* [utility/to_underlying](https://en.cppreference.com/w/cpp/utility/to_underlying) +* [Enumeration declarations and conversions](https://en.cppreference.com/w/cpp/language/enum) +* [Explicit \`static_cast\` to an enum](https://en.cppreference.com/w/cpp/language/static_cast) +* [\`std::underlying_type\`](https://en.cppreference.com/w/cpp/types/underlying_type) +* [\`std::to_underlying\` (C++23)](https://en.cppreference.com/w/cpp/utility/to_underlying) +* [\`std::is_scoped_enum\` (C++23)](https://en.cppreference.com/w/cpp/types/is_scoped_enum) +* [\`std::meta::enumerators_of\` (C++26)](https://en.cppreference.com/w/cpp/meta/enumerators_of) + +### Proposals and guidelines + +* [P2996 — Reflection for C++26](https://wg21.link/P2996) +* [C++ Core Guidelines, Enum: Enumerations](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-enum) +* [WG21 CWG 1766: Out-of-range enum conversions](https://cplusplus.github.io/CWG/issues/1766.html) + +### Articles and tools + +* [Compiler Explorer](https://godbolt.org/) — compare warnings, representations, and optimization assumptions +* [magic_enum](https://github.com/Neargye/magic_enum) — example of third-party pre-reflection enum inspection with implementation constraints From 97c2e946450c312bd7698570c322fce3f66b171e Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:24:48 +0700 Subject: [PATCH 2/4] fix: avoid undefined out-of-range enum cast in core example --- .../07_enums/01_enums_core/enums_core.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/01_low_level/07_enums/01_enums_core/enums_core.cpp b/01_low_level/07_enums/01_enums_core/enums_core.cpp index 396411a..85934e3 100644 --- a/01_low_level/07_enums/01_enums_core/enums_core.cpp +++ b/01_low_level/07_enums/01_enums_core/enums_core.cpp @@ -13,10 +13,9 @@ // - An enum is a distinct type, but it implicitly converts to int (unscoped enum). // - Underlying type for unscoped enums is implementation-defined (usually int). // - Enumerators are integral constant expressions. -// - You can cast any integer to an enum type; the resulting value may not correspond -// to any enumerator. That is not automatically UB just by storing it, but using it -// as if it were a valid enumerator is a logic bug (and may cause undefined behavior -// if you rely on invariants that assume only listed enumerators occur). +// - A non-fixed enum has a restricted value range. Casting an out-of-range +// integer to it has undefined behavior (CWG 1766). An in-range but unnamed +// value is possible; fixed-underlying enums can hold every underlying value. // - Size of an unscoped enum is implementation-defined (but at least enough to represent values). // // Evolution: @@ -61,14 +60,15 @@ static void casting_outside_enumerators() PlainSmall ok = static_cast(0); assert(ok == ps0); - // This produces a value of type PlainSmall that is not equal to any named enumerator. - PlainSmall weird = static_cast(20); + // A non-fixed enum with values 1, 2, 4 has a range that includes 3. + // The cast is in range and creates an unnamed, well-defined enum value. + enum Flags { read = 1, write = 2, execute = 4 }; + Flags combined = static_cast(3); + assert(static_cast(combined) == 3); - // IMPORTANT: - // - It's not "automatically UB" just to have this value. - // - But any code that assumes only ps0/ps1 exist is now broken. - // - In practice: validate when converting from external input. - (void)weird; + // static_cast(20) would be undefined behavior: 20 is + // outside PlainSmall's language-defined value range. + // Validate external integers before converting to a non-fixed enum. } // Demonstrate why "enums are not just ints" (types differ even if representation matches). From e9bc278f4712ed4100cf30951463bc067aaa08ab Mon Sep 17 00:00:00 2001 From: Yurii Cherkasov Date: Mon, 19 Oct 2026 00:24:50 +0700 Subject: [PATCH 3/4] fix: explain scoped enum defaults and opaque completeness --- .../07_enums/02_enums_scoped/enums_scoped.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/01_low_level/07_enums/02_enums_scoped/enums_scoped.cpp b/01_low_level/07_enums/02_enums_scoped/enums_scoped.cpp index 2a40ef9..ba06571 100644 --- a/01_low_level/07_enums/02_enums_scoped/enums_scoped.cpp +++ b/01_low_level/07_enums/02_enums_scoped/enums_scoped.cpp @@ -14,8 +14,9 @@ // - Underlying type can be specified: enum class E : std::uint8_t { ... } // - Scoped enums have a fixed underlying type if specified; otherwise still integral but chosen by compiler. // - Forward declaration: -// * scoped enums can always be forward-declared (with or without underlying type) -// but to be a complete type, it must be defined. +// * an opaque scoped enum declaration is already a complete type because +// its underlying type is fixed (int by default). Enumerator names still +// require the later definition. // * unscoped enums can only be forward-declared if you specify the underlying type. // // Evolution notes: @@ -25,7 +26,7 @@ enum class TrafficLight : std::uint8_t { Green = 1, Yellow = 2, Red = 3 }; // Forward decl scoped enum: OK -enum class ErrorCode; // incomplete type until definition +enum class ErrorCode; // complete opaque enum type; enumerators not yet known // Forward decl unscoped enum: requires underlying type (C++11+) enum LegacyStatus : unsigned int; @@ -87,12 +88,11 @@ static void underlying_type_facts() static void forward_decl_rules() { - // You can declare pointers/references to incomplete enum types: + // An opaque enum declaration fixes the underlying type and makes + // the type complete already; only its enumerators remain unknown. ErrorCode* p = nullptr; + static_assert(sizeof(ErrorCode) == sizeof(int)); (void)p; - - // But you cannot sizeof an incomplete type: - // sizeof(ErrorCode); // ERROR if only forward-declared } int main() From 502defda2f056aff008577cf86921a8fd8bc9a77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:09:04 +0000 Subject: [PATCH 4/4] Fix Chapter 07 Markdown and review comments Co-authored-by: yuchdev <678436+yuchdev@users.noreply.github.com> --- 01_low_level/07_enums/README.md | 312 ++++++++++++++++---------------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/01_low_level/07_enums/README.md b/01_low_level/07_enums/README.md index cc7f725..73a6099 100644 --- a/01_low_level/07_enums/README.md +++ b/01_low_level/07_enums/README.md @@ -2,16 +2,16 @@ An enumeration is a distinct C++ type with an integral underlying representation. That definition looks simple until one tries to answer which integer values an enum can hold, how it participates in arithmetic, why a seemingly innocuous cast can be undefined behavior, or whether a compiler can prove that a switch covers every meaningful state. The old C-style enumeration was built for named integer constants; modern scoped enumerations are intended to make semantic domains visible in the type system. -For experienced C++ developers, the difficult questions are not how to spell \`enum class\), but where the language draws the boundaries between an enum's *enumerators*, its *value range*, its *underlying type*, and the *domain* accepted by an application. Those boundaries matter for protocols, bitmasks, overloads, FFI, ABI evolution, optimization, and exhaustive state machines. +For experienced C++ developers, the difficult questions are not how to spell `enum class`, but where the language draws the boundaries between an enum's *enumerators*, its *value range*, its *underlying type*, and the *domain* accepted by an application. Those boundaries matter for protocols, bitmasks, overloads, FFI, ABI evolution, optimization, and exhaustive state machines. This chapter develops the subject historically—from C++98 unscoped enums to C++26 compile-time reflection—while following the four source demonstrations in this directory: -1. [\`01_enums_core/enums_core.cpp\`](01_enums_core/enums_core.cpp) — unscoped enums, integer promotions, sizes, distinct types, and casts. -2. [\`02_enums_scoped/enums_scoped.cpp\`](02_enums_scoped/enums_scoped.cpp) — scoped enums, explicit representation, forward declarations, and conversions. -3. [\`03_enums_operations/enums_operations.cpp\`](03_enums_operations/enums_operations.cpp) — typed bit flags, overloaded operators, and masking. -4. [\`04_modern_enums/modern_enums.cpp\`](04_modern_enums/modern_enums.cpp) — state decoding, switches, and C++20 \`using enum\`. +1. [`01_enums_core/enums_core.cpp`](01_enums_core/enums_core.cpp) — unscoped enums, integer promotions, sizes, distinct types, and casts. +2. [`02_enums_scoped/enums_scoped.cpp`](02_enums_scoped/enums_scoped.cpp) — scoped enums, explicit representation, forward declarations, and conversions. +3. [`03_enums_operations/enums_operations.cpp`](03_enums_operations/enums_operations.cpp) — typed bit flags, overloaded operators, and masking. +4. [`04_modern_enums/modern_enums.cpp`](04_modern_enums/modern_enums.cpp) — state decoding, switches, and C++20 `using enum`. -The repository targets C++20. Code from C++23 and C++26 is either shown under explicit feature-test guards or labeled as a later-standard illustration; support for C++26 reflection is not presumed merely because a compiler accepts \`-std=c++26\`. +The repository targets C++20. Code from C++23 and C++26 is either shown under explicit feature-test guards or labeled as a later-standard illustration; support for C++26 reflection is not presumed merely because a compiler accepts `-std=c++26`. --- @@ -23,7 +23,7 @@ At the machine level, an enum usually occupies storage in an integer representat Consider status and device-state values that happen to share their representation. There is no implicit assignment from one enum type to another, and this is intentional: the number alone does not define its meaning. -\`\`\`cpp +```cpp #include enum Status { ready = 0, failed = 1 }; @@ -31,27 +31,27 @@ enum DeviceState { offline = 0, online = 1 }; static_assert(!std::is_same_v); // DeviceState d = ready; // ill-formed: distinct enum types -\`\`\` +``` -An unscoped enumerator such as \`ready\` participates in arithmetic through implicit integral conversion, but its enumeration type remains distinct. Two enums sharing an underlying type are not interchangeable API domains. +An unscoped enumerator such as `ready` participates in arithmetic through implicit integral conversion, but its enumeration type remains distinct. Two enums sharing an underlying type are not interchangeable API domains. ### Enumerators are compile-time constants, not stored fields Each named enumerator is a constant of the enum type, usable where a constant expression is required. Its value may be implicit or explicitly assigned, and later unassigned enumerators increment the preceding value. -\`\`\`cpp +```cpp enum Error { ok = 0, retry = 4, timeout, abandoned = timeout }; static_assert(timeout == 5); static_assert(abandoned == timeout); int retry_buffer[retry]{}; // constant bound -\`\`\` +``` Nothing about this definition builds a runtime lookup table, reflection structure, or mapping from integers to names. Duplicate enumerator *values* are allowed; duplicate enumerator *names* in the same scope are not. ### Storage representation is not the complete semantic domain -When \`sizeof(E)\` is four, that alone does not mean every 32-bit pattern is a valid value of an enum without a fixed underlying type. The underlying integer *representation* and the enum's permitted *value range* are related but different concepts. Section 4 develops the distinction because it affects \`static_cast\`, sanitizer behavior, and wire-protocol parsing. +When `sizeof(E)` is four, that alone does not mean every 32-bit pattern is a valid value of an enum without a fixed underlying type. The underlying integer *representation* and the enum's permitted *value range* are related but different concepts. Section 4 develops the distinction because it affects `static_cast`, sanitizer behavior, and wire-protocol parsing. --- @@ -63,50 +63,50 @@ The C++98 form inherits much of C's original intent: a set of related named cons An unscoped enum introduces its enumerator names into the surrounding scope. The enum type itself can be named and used in declarations, but the enumerators are not protected from collisions with unrelated constants. -\`\`\`cpp +```cpp namespace graphics { enum Color { red, green, blue }; Color current = red; } // enum Other { red, yellow }; // conflicts if placed in graphics -\`\`\` +``` Namespaces and class scopes reduce collision risk, but do not replace the stronger encapsulation of scoped enumerators. Modern code can still use unscoped enums deliberately when legacy C interfaces or integral conversion behavior is part of the contract. ### Integral promotion can change both type and arithmetic behavior -An unscoped enum with a non-fixed underlying type promotes according to the integer promotion rules. It may become \`int\`, or another suitable integer type when its representable range does not fit. A fixed-underlying unscoped enum also permits conversion to its underlying type, with consequences for overload resolution. +An unscoped enum with a non-fixed underlying type promotes according to the integer promotion rules. It may become `int`, or another suitable integer type when its representable range does not fit. A fixed-underlying unscoped enum also permits conversion to its underlying type, with consequences for overload resolution. -\`\`\`cpp +```cpp enum Position { left = 0, center = 1, right = 2 }; static_assert(std::is_same_v); int adjusted = right + 10; -\`\`\` +``` -The result is an integer, not a newly constructed \`Position\`. This makes accidental arithmetic easy, and it is a reason to prefer scoped enums for states rather than numeric quantities. +The result is an integer, not a newly constructed `Position`. This makes accidental arithmetic easy, and it is a reason to prefer scoped enums for states rather than numeric quantities. ### The usual arithmetic conversions are not enum-preserving Once an unscoped enum enters a mixed arithmetic expression, the ordinary conversion machinery determines the result type. Signedness and integer rank can matter as much as the enum declaration itself. Treat warning-free arithmetic as insufficient evidence that an enum still denotes one of the application's defined states. -\`\`\`cpp +```cpp enum Phase { initial = 1, final = 3 }; auto next = final + 1; // integral result, not Phase static_assert(std::is_integral_v); -\`\`\` +``` If an algorithm genuinely needs modular arithmetic over a numeric domain, a plain integer or a deliberately designed wrapper may represent the requirement better than an enum. ### Unscoped enumerators can still be qualified in modern C++ -C++11 permits the \`E::member\` spelling for unscoped enumerators. Qualification improves readability and resolves ambiguities without transforming the enum into a scoped enum. +C++11 permits the `E::member` spelling for unscoped enumerators. Qualification improves readability and resolves ambiguities without transforming the enum into a scoped enum. -\`\`\`cpp +```cpp enum LegacyResult { success, failure }; auto result = LegacyResult::success; // C++11 -\`\`\` +``` This is particularly useful while modernizing legacy code incrementally, because the type and implicit conversions remain unchanged. @@ -114,27 +114,27 @@ This is particularly useful while modernizing legacy code incrementally, because ## 3. C++11 scoped enumerations establish a stronger API boundary -C++11 introduced \`enum class\` and \`enum struct\` as equivalent scoped-enumeration spellings. Their purpose is to prevent accidental name injection and implicit conversion to integer types. They do not, however, automatically validate external input. +C++11 introduced `enum class` and `enum struct` as equivalent scoped-enumeration spellings. Their purpose is to prevent accidental name injection and implicit conversion to integer types. They do not, however, automatically validate external input. ### Scoped enumerators stay inside the enum scope The distinction is immediately visible when two domains use the same common words. -\`\`\`cpp +```cpp enum class Traffic { red, green }; enum class Alert { red, green }; Traffic light = Traffic::green; // int x = light; // ill-formed -\`\`\` +``` Scoped enums work well for state machines and typed options because an integer operation requires the programmer to explicitly cross the type boundary. -### The default underlying type of a scoped enum is \`int\` +### The default underlying type of a scoped enum is `int` -The declaration \`enum class Mode { idle, running };\` has \`int\` as its underlying type. It is not an implementation-selected arbitrary integer type. The underlying type may instead be specified explicitly, subject to the rules for an integral underlying type. +The declaration `enum class Mode { idle, running };` has `int` as its underlying type. It is not an implementation-selected arbitrary integer type. The underlying type may instead be specified explicitly, subject to the rules for an integral underlying type. -\`\`\`cpp +```cpp #include #include @@ -144,20 +144,20 @@ enum class WireMode : std::uint8_t { idle, running }; static_assert(std::is_same_v, int>); static_assert(std::is_same_v< std::underlying_type_t, std::uint8_t>); -\`\`\` +``` An explicit fixed type is valuable when another ABI or representation contract requires particular width and signedness. A fixed underlying type alone does not define a portable byte-order serialization format. ### Scoped types can have unnamed values -\`enum class\` prevents implicit integer conversions, not explicit casts or construction from certain integer values. The type still has a range tied to its fixed underlying representation, and the range can be wider than the named enumerators. +`enum class` prevents implicit integer conversions, not explicit casts or construction from certain integer values. The type still has a range tied to its fixed underlying representation, and the range can be wider than the named enumerators. -\`\`\`cpp +```cpp enum class Mode : std::uint8_t { idle = 0, active = 1 }; Mode unknown = static_cast(200); // defined, but unnamed -\`\`\` +``` -A domain parser should decide whether \`200\` is rejected, reserved for forward compatibility, or mapped to an explicit unknown state. Type safety protects the interface from accidental mixing; it does not provide schema validation. +A domain parser should decide whether `200` is rejected, reserved for forward compatibility, or mapped to an explicit unknown state. Type safety protects the interface from accidental mixing; it does not provide schema validation. --- @@ -169,26 +169,26 @@ This is the most dangerous subtlety in classic enum code. A fixed-underlying enu For an unscoped enum without a fixed underlying type, the language defines a representable range based on the smallest bit-field width sufficient for its enumerators, not necessarily the full machine range of its storage type. The range need not be the closed interval between the smallest and largest named values. -\`\`\`cpp +```cpp enum Permission { read = 1, write = 2, execute = 4 }; Permission combination = static_cast(3); // in range; unnamed // auto invalid = static_cast(8); // undefined behavior -\`\`\` +``` -A commonly observed implementation may retain the bits of \`8\` and appear to work, but the cast is not a portable operation. CWG 1766 clarified the out-of-range rule as undefined behavior; implementations and standards publications may describe its historical application differently. +A commonly observed implementation may retain the bits of `8` and appear to work, but the cast is not a portable operation. CWG 1766 clarified the out-of-range rule as undefined behavior; implementations and standards publications may describe its historical application differently. ### Fixed underlying types permit every representable underlying value When the underlying type is fixed, the enum's value range is that of the underlying type. Conversion from an integer uses the corresponding underlying-type conversion first. -\`\`\`cpp +```cpp enum class Permission : std::uint8_t { read = 1, write = 2, execute = 4 }; auto unknown = static_cast(200); // defined unnamed value -\`\`\` +``` This makes fixed-underlying enums suitable for preserving unknown protocol codes. It still does not make every such value *semantically valid* for an application. @@ -196,20 +196,20 @@ This makes fixed-underlying enums suitable for preserving unknown protocol codes An enum object holding a representable value that lacks a name remains an enum object. That can be useful for bitmask combinations or extensible protocols. The correct classification has at least three layers: representable by the enum type; named by an enumerator; accepted by the application. -\`\`\`cpp +```cpp enum class State : unsigned char { stopped = 0, started = 1 }; State s = static_cast(2); // representable but unnamed bool known(State x) { return x == State::stopped || x == State::started; } -\`\`\` +``` Conflating these layers is dangerous at trust boundaries. Validate integers *before* converting to a non-fixed enum if they may be outside its language-defined range. ### Never use an out-of-range example as a harmless demonstration -The existing \`01_enums_core/enums_core.cpp\` example casts \`20\` to an enum whose enumerators are only \`0\` and \`1\`. Because that enum has no fixed underlying type, it is not a valid demonstration of merely holding an unnamed enum value. It needs correction in the accompanying source example as well as in this chapter. +The `01_enums_core/enums_core.cpp` example demonstrates a safe in-range unnamed value: it casts `3` to a non-fixed enum whose enumerators are `1`, `2`, and `4`. The invalid out-of-range cast of `20` remains only as a warning in a comment. The safe alternative is an in-range unnamed value on an enum whose range has room for one, or a fixed-underlying enum. A sanitizer need not diagnose every such invalid cast; correctness comes from the specified value-range contract. @@ -221,33 +221,33 @@ An enum is a proper object type with alignment and size, and its layout can affe ### An unscoped enum without a fixed underlying type leaves representation to the implementation -The implementation chooses a compatible underlying integer type that can represent the enumerators, subject to the language rules. Relying on an observed \`sizeof\` in one build is not sufficient to design a stable library ABI. +The implementation chooses a compatible underlying integer type that can represent the enumerators, subject to the language rules. Relying on an observed `sizeof` in one build is not sufficient to design a stable library ABI. -\`\`\`cpp +```cpp enum LegacyCode { ok = 0, error = 70000 }; // sizeof(LegacyCode) is not a portable protocol width. -\`\`\` +``` -Compiler switches such as \`-fshort-enums\` can also affect layout decisions and break binary compatibility between compilation units. Such options belong in a documented whole-program ABI contract. +Compiler switches such as `-fshort-enums` can also affect layout decisions and break binary compatibility between compilation units. Such options belong in a documented whole-program ABI contract. ### Fixed underlying type implies matching size and representation A fixed-underlying enum has the same size, value representation, and alignment as its specified underlying type under the standard's enum rules. It does not guarantee the host machine's byte order or the ABI calling convention of a surrounding library. -\`\`\`cpp +```cpp enum class PacketKind : std::uint16_t { data = 1, ack = 2 }; static_assert(sizeof(PacketKind) == sizeof(std::uint16_t)); static_assert(alignof(PacketKind) == alignof(std::uint16_t)); -\`\`\` +``` -The specific fixed-width integer typedef must exist for the target. \`std::uint16_t\` is not required on a machine without a suitable exact-width integer type. +The specific fixed-width integer typedef must exist for the target. `std::uint16_t` is not required on a machine without a suitable exact-width integer type. ### Treat public ABI, persistence format, and source API as three separate contracts A public C++ function taking an enum has an ABI influenced by the target, toolchain, and compiler options. A stored protocol field has byte order and versioning requirements. A C++ source API has name lookup and conversion rules. Fixing an underlying type helps, but does not collapse these three independent questions into one guarantee. -A robust protocol reader extracts a specified-width integer from bytes, validates or preserves it according to the schema, and then constructs the enum. Raw \`memcpy\` of a struct containing enums is not a platform-independent wire format. +A robust protocol reader extracts a specified-width integer from bytes, validates or preserves it according to the schema, and then constructs the enum. Raw `memcpy` of a struct containing enums is not a platform-independent wire format. --- @@ -257,27 +257,27 @@ Before C++11, arbitrary enum types could not be forward-declared in the way clas ### A scoped enum can be declared before listing enumerators -Because the default underlying type is \`int\`, this opaque declaration fixes enough representation information to form objects and apply \`sizeof\`. +Because the default underlying type is `int`, this opaque declaration fixes enough representation information to form objects and apply `sizeof`. -\`\`\`cpp +```cpp enum class ErrorCode; static_assert(sizeof(ErrorCode) == sizeof(int)); enum class ErrorCode { success, failure }; -\`\`\` +``` -This is not analogous to a forward-declared incomplete class. Code cannot refer to \`ErrorCode::success\` before the definition, but the representation and completeness rules allow layout-sensitive declarations. +This is not analogous to a forward-declared incomplete class. Code cannot refer to `ErrorCode::success` before the definition, but the representation and completeness rules allow layout-sensitive declarations. ### Unscoped opaque enums require an explicit underlying type -The C++11 declaration \`enum Legacy : unsigned;\` is permitted. Omitting its underlying type in an opaque unscoped declaration is not. +The C++11 declaration `enum Legacy : unsigned;` is permitted. Omitting its underlying type in an opaque unscoped declaration is not. -\`\`\`cpp +```cpp enum Legacy : unsigned; Legacy make_legacy(); // legal declaration enum Legacy : unsigned { zero = 0, one = 1 }; -\`\`\` +``` Every subsequent definition must agree with the underlying type. This pattern can reduce header dependencies when enumerator names are needed in only a few implementation files. @@ -291,26 +291,26 @@ The boundary between enums and integers is asymmetric. Unscoped enums convert im An overload set can contain both enum-aware and integer-aware functions. Passing an enum selects the exact match, but removing that overload can silently cause legacy unscoped code to call the integral overload. -\`\`\`cpp +```cpp enum LegacyMode { on, off }; void configure(LegacyMode); void configure(int); void use() { configure(on); } // chooses configure(LegacyMode) -\`\`\` +``` The risk is not that enums are aliases for integers; the risk is that implicit conversion makes an unintended fallback viable. ### Scoped enums do not participate in implicit arithmetic -Unlike \`LegacyMode\`, a scoped \`enum class\` does not implicitly select an \`int\` overload and does not accept \`operator+\` from the built-in arithmetic rules. +Unlike `LegacyMode`, a scoped `enum class` does not implicitly select an `int` overload and does not accept `operator+` from the built-in arithmetic rules. -\`\`\`cpp +```cpp enum class Mode { on, off }; // auto n = Mode::on + 1; // ill-formed // configure(Mode::on); // ill-formed without a suitable overload -\`\`\` +``` A named explicit conversion is clearer than a C-style cast because it makes the crossing of abstraction boundaries visible in code review. @@ -318,31 +318,31 @@ A named explicit conversion is clearer than a C-style cast because it makes the C++17 permits direct-list initialization of a scoped enum, and of an unscoped enum with a fixed underlying type, from an integral value when the initialization rules permit it. This is not implicit conversion and is not a license for narrowing. -\`\`\`cpp +```cpp enum class ByteKind : unsigned char {}; ByteKind one{1}; // C++17 // ByteKind too_large{300}; // narrowing: ill-formed -\`\`\` +``` The rule supports small strong integer wrappers and selected low-level APIs. A named enumerator or parsing function is still preferable when an application requires a closed domain. -### C++23 \`std::to_underlying\` names the intended conversion +### C++23 `std::to_underlying` names the intended conversion -Before C++23, code typically used \`static_cast>(value)\` or a short helper. The standard library now provides \`std::to_underlying\` in \`\`. +Before C++23, code typically used `static_cast>(value)` or a short helper. The standard library now provides `std::to_underlying` in ``. -\`\`\`cpp +```cpp #include #include enum class Code : unsigned { ok = 0, error = 17 }; constexpr auto raw(Code code) noexcept { - return static_cast>(code); // C++11+ + return static_cast>(code); // C++14+ } #if defined(__cpp_lib_to_underlying) && __cpp_lib_to_underlying >= 202102L static_assert(std::to_underlying(Code::error) == 17u); #endif -\`\`\` +``` It extracts the representation value; it does not validate whether the enum is one of the named values. @@ -350,13 +350,13 @@ It extracts the representation value; it does not validate whether the enum is o ## 8. Bitmask enums require a different domain model from state enums -A state enum usually denotes *one of several mutually exclusive states*. A bitmask enum usually denotes *any allowed combination* of independent flags. If both are modeled by \`enum class\`, their common syntax can conceal a major semantic difference. +A state enum usually denotes *one of several mutually exclusive states*. A bitmask enum usually denotes *any allowed combination* of independent flags. If both are modeled by `enum class`, their common syntax can conceal a major semantic difference. ### Choose non-overlapping bits and an unsigned underlying type -A permission flag set can use individual bits, usually with an explicitly unsigned fixed representation. A name such as \`all\` should mean exactly the OR of supported flags, not simply all bits of the storage type. +A permission flag set can use individual bits, usually with an explicitly unsigned fixed representation. A name such as `all` should mean exactly the OR of supported flags, not simply all bits of the storage type. -\`\`\`cpp +```cpp enum class Permission : std::uint32_t { none = 0, read = 1u << 0, @@ -364,21 +364,21 @@ enum class Permission : std::uint32_t { execute = 1u << 2, all = (1u << 0) | (1u << 1) | (1u << 2) }; -\`\`\` +``` An unsigned representation makes shifts and mask operations easier to reason about. Always ensure the shift count is less than the underlying width and that the left operand has a suitable unsigned type. ### Constrain bitwise operators to the intended enum -Do not add generic bitwise operators to every enum; doing so makes nonsensical operations such as \`Traffic::red | Traffic::green\` compile. A focused overload keeps permissions combinable without weakening unrelated state types. +Do not add generic bitwise operators to every enum; doing so makes nonsensical operations such as `Traffic::red | Traffic::green` compile. A focused overload keeps permissions combinable without weakening unrelated state types. -\`\`\`cpp +```cpp constexpr Permission operator|(Permission a, Permission b) noexcept { using U = std::underlying_type_t; return static_cast( static_cast(a) | static_cast(b)); } -\`\`\` +``` A reusable flag-operator library can use an opt-in trait or concept to constrain overloads, but it must still define the legal mask and avoid unexpected argument-dependent lookup behavior. @@ -386,42 +386,42 @@ A reusable flag-operator library can use an opt-in trait or concept to constrain The complement of the numeric underlying value flips all bits, including bits with no named flag. This is not a type error because the fixed underlying enum can represent them, but it may violate the application-level mask domain. -\`\`\`cpp +```cpp constexpr Permission without_read(Permission p) noexcept { using U = std::underlying_type_t; constexpr U valid = static_cast(Permission::all); return static_cast( static_cast(p) & (valid ^ static_cast(Permission::read))); } -\`\`\` +``` -The example removes a known bit without introducing unknown ones. When parsing flags from a protocol, validate \`(raw & ~valid_mask) == 0\` or define a deliberate rule for forward-compatible extensions. +The example removes a known bit without introducing unknown ones. When parsing flags from a protocol, validate `(raw & ~valid_mask) == 0` or define a deliberate rule for forward-compatible extensions. ### Testing for any flags is different from testing for all flags A common helper returns true if *any* requested bit is present. Applications often also need a separate helper that tests whether *every* requested bit is present. -\`\`\`cpp +```cpp constexpr bool contains_all(Permission actual, Permission required) { using U = std::underlying_type_t; U a = static_cast(actual), r = static_cast(required); return (a & r) == r; } -\`\`\` +``` -The zero-mask case follows naturally: every set contains all bits of \`none\`. Whether that is desirable must be settled by the API contract. +The zero-mask case follows naturally: every set contains all bits of `none`. Whether that is desirable must be settled by the API contract. --- ## 9. Switch exhaustiveness is a diagnostic policy, not a language guarantee -A \`switch\` on an enum is expressive precisely because the compiler can know the set of *named* enumerators. The compiler does not, however, guarantee that the switch handles every representable enum value. The best design separates missing named cases from intentionally unknown input. +A `switch` on an enum is expressive precisely because the compiler can know the set of *named* enumerators. The compiler does not, however, guarantee that the switch handles every representable enum value. The best design separates missing named cases from intentionally unknown input. ### The absence of a default may reveal newly added enumerators -For internal closed state machines, a switch with explicit cases and an unreachable fallback after the switch can allow \`-Wswitch\` to warn when a new enumerator appears. A \`default\` branch can hide that diagnostic with some warning modes. +For internal closed state machines, a switch with explicit cases and an unreachable fallback after the switch can allow `-Wswitch` to warn when a new enumerator appears. A `default` branch can hide that diagnostic with some warning modes. -\`\`\`cpp +```cpp enum class Mode { idle, running, stopped }; const char* name(Mode m) { @@ -432,15 +432,15 @@ const char* name(Mode m) { } return ""; } -\`\`\` +``` The final return is still essential because a fixed-underlying enum can hold an unnamed value. For genuinely unreachable internal states, fail explicitly instead of returning a plausible but incorrect state. ### A wire decoder should validate integers before dispatch -Do not make a \`switch\` over an unchecked cast the sole source of validation. Parse the incoming integer and map only accepted codes to enum values, so the distinction between known, reserved, and invalid inputs remains visible. +Do not make a `switch` over an unchecked cast the sole source of validation. Parse the incoming integer and map only accepted codes to enum values, so the distinction between known, reserved, and invalid inputs remains visible. -\`\`\`cpp +```cpp #include std::optional decode(unsigned raw) { @@ -451,15 +451,15 @@ std::optional decode(unsigned raw) { default: return std::nullopt; } } -\`\`\` +``` -Using \`std::optional\` is a C++17 option. C++98 code can express the same contract through a boolean return and output parameter. Explicit parsing is more stable than assuming every integer cast produces a meaningful enumerator. +Using `std::optional` is a C++17 option. C++98 code can express the same contract through a boolean return and output parameter. Explicit parsing is more stable than assuming every integer cast produces a meaningful enumerator. -### C++20 \`using enum\` is a local convenience with lookup consequences +### C++20 `using enum` is a local convenience with lookup consequences -C++20 \`using enum\` makes enumerators available in the current scope, including scoped-enum members. This can make a short switch or local dispatch table readable, but it can also recreate the name-collision risks avoided by scoped enums. +C++20 `using enum` makes enumerators available in the current scope, including scoped-enum members. This can make a short switch or local dispatch table readable, but it can also recreate the name-collision risks avoided by scoped enums. -\`\`\`cpp +```cpp void handle(Mode m) { using enum Mode; switch (m) { @@ -468,7 +468,7 @@ void handle(Mode m) { case stopped: break; } } -\`\`\` +``` Prefer short local scopes. Avoid importing common enumerator names into public headers or broadly shared namespaces. @@ -478,26 +478,26 @@ Prefer short local scopes. Avoid importing common enumerator names into public h An enum's type-level identity can be used to select overloads, control serialization, and write reusable formatting helpers. Each standard added vocabulary that reduces the need for handwritten detection code, but it remains easy to confuse a type trait with actual enumerator introspection. -### \`std::underlying_type\` exposes representation types, not enumerator lists +### `std::underlying_type` exposes representation types, not enumerator lists -C++11 \`std::underlying_type\` and C++14 \`std::underlying_type_t\` are useful for converting enum values to the chosen underlying integer representation. +C++11 `std::underlying_type` and C++14 `std::underlying_type_t` are useful for converting enum values to the chosen underlying integer representation. -\`\`\`cpp +```cpp template constexpr auto representation(E e) noexcept -> std::underlying_type_t { return static_cast>(e); } -\`\`\` +``` -The trait does not reveal the number of enumerators, their identifiers, or gaps in their values. A general-purpose enumeration iteration library cannot be implemented from \`underlying_type\` alone. +The trait does not reveal the number of enumerators, their identifiers, or gaps in their values. A general-purpose enumeration iteration library cannot be implemented from `underlying_type` alone. ### C++20 concepts can restrict templates to enumeration types -Generic formatting or serialization functions often need a deliberate constraint to avoid accepting ordinary integers by accident. \`std::is_enum_v\` has existed since C++17, and C++20 concepts make a constraint visible at an interface. +Generic formatting or serialization functions often need a deliberate constraint to avoid accepting ordinary integers by accident. `std::is_enum_v` has existed since C++17, and C++20 concepts make a constraint visible at an interface. -\`\`\`cpp +```cpp template concept Enumeration = std::is_enum_v; @@ -505,20 +505,20 @@ template constexpr auto enum_bits(E e) { return static_cast>(e); } -\`\`\` +``` -The concept here is user-defined; C++20 does not provide a built-in \`std::enum_type\` concept. A constraint says what types are admitted, not whether an enum is intended as a flag set. +The concept here is user-defined; C++20 does not provide a built-in `std::enum_type` concept. A constraint says what types are admitted, not whether an enum is intended as a flag set. -### C++23 \`std::is_scoped_enum\` separates the two language categories +### C++23 `std::is_scoped_enum` separates the two language categories -C++23 adds \`std::is_scoped_enum\`, which allows generic code to distinguish classic unscoped enums from scoped enums without guessing from convertibility to \`int\`. +C++23 adds `std::is_scoped_enum`, which allows generic code to distinguish classic unscoped enums from scoped enums without guessing from convertibility to `int`. -\`\`\`cpp +```cpp #if defined(__cpp_lib_is_scoped_enum) && __cpp_lib_is_scoped_enum >= 202011L static_assert(std::is_scoped_enum_v); static_assert(!std::is_scoped_enum_v); #endif -\`\`\` +``` This is useful for modernizing generic APIs and applying different policies to legacy integer-compatible enums. It still does not tell whether a particular scoped enum is a bitmask. @@ -532,13 +532,13 @@ Enums appear frequently in protocol fields, configuration files, persisted state Implicit numbering is convenient until someone inserts a new enumerator in the middle. Any protocol, file format, or external API should assign stable numeric IDs deliberately and document reserved ranges. -\`\`\`cpp +```cpp enum class Message : std::uint16_t { hello = 1, payload = 10, goodbye = 100 }; -\`\`\` +``` Never treat enum ordinal position or contiguous naming as a portable serialization scheme. Aliases can even share the same number, defeating one-to-one inverse mappings. @@ -556,16 +556,16 @@ For a stable C ABI, expose the exact documented scalar representation and conver ### Concurrency safety does not follow from using enums -An enum can be a natural state stored in \`std::atomic\` when the type meets the required conditions, but atomic reads and writes do not guarantee that transitions obey a protocol. +An enum can be a natural state stored in `std::atomic` when the type meets the required conditions, but atomic reads and writes do not guarantee that transitions obey a protocol. -\`\`\`cpp +```cpp #include enum class State : unsigned char { idle, active, stopped }; std::atomic state{State::idle}; -\`\`\` +``` -The atomic prevents data races on that object under valid use; it does not enforce legal transitions such as prohibiting \`stopped -> active\`. Use compare-exchange loops or a synchronized state machine when transition invariants matter. +The atomic prevents data races on that object under valid use; it does not enforce legal transitions such as prohibiting `stopped -> active`. Use compare-exchange loops or a synchronized state machine when transition invariants matter. --- @@ -575,11 +575,11 @@ Before standard reflection, enumerator-name utilities typically relied on mainta ### Reflection exposes enumerators rather than guessed integer ranges -C++26 reflection introduces \`\` and \`std::meta::enumerators_of\` to obtain reflections of the enumerators of an enum type in declaration order. This is categorically different from scanning all possible integral values or inferring names from vendor function-signature strings. +C++26 reflection introduces `` and `std::meta::enumerators_of` to obtain reflections of the enumerators of an enum type in declaration order. This is categorically different from scanning all possible integral values or inferring names from vendor function-signature strings. The following is **C++26 reflection syntax**, not C++20-compatible code: -\`\`\`cpp +```cpp // C++26 only, when reflection and are supported: #include #include @@ -588,19 +588,19 @@ enum class Fruit { apple = 1, pear = 8, plum = 8 }; consteval auto entries() { return std::meta::enumerators_of(^^Fruit); } -\`\`\` +``` The result describes *declarations*, which means aliases with equal numeric values may both appear. Applications must still choose whether a value-to-name conversion prefers the first declared alias, all aliases, or a canonical one. ### Name extraction and value extraction have separate semantics -A reflection of an enumerator can be used with the appropriate \`std::meta\` facilities to obtain identifier spelling and to generate a value-producing expression. This makes it possible to build compile-time tables without writing the same enumerator list twice. +A reflection of an enumerator can be used with the appropriate `std::meta` facilities to obtain identifier spelling and to generate a value-producing expression. This makes it possible to build compile-time tables without writing the same enumerator list twice. Reflection is not a free runtime registry: it is a compile-time facility used by code-generation and constant-evaluation mechanisms. The generated tables and strings still consume program resources and may require design choices about linkage and binary size. ### Compiler and standard-library support must be feature-tested -C++26 reflection has a feature-test macro, and the library's \`\` interface must actually be available. The spelling and implemented subset of these facilities can evolve as toolchains adopt the standard. +C++26 reflection has a feature-test macro, and the library's `` interface must actually be available. The spelling and implemented subset of these facilities can evolve as toolchains adopt the standard. Keep reflection examples in C++26-only blocks or separate demonstration sources. For the C++20 baseline of this repository, retain explicit tables or a documented external enum-reflection library, and avoid claiming that old C++ compilers can enumerate identifiers portably. @@ -622,7 +622,7 @@ If an untrusted number is cast to a non-fixed unscoped enum outside its value ra ### A fixed underlying type does not guarantee named values -\`enum class Mode : std::uint8_t\` can hold unnamed values such as \`200\`. Robust switching, logging, and serialization must have a policy for these cases, even when every named enumerator is handled. +`enum class Mode : std::uint8_t` can hold unnamed values such as `200`. Robust switching, logging, and serialization must have a policy for these cases, even when every named enumerator is handled. ### A flags type is not necessarily a state type @@ -630,7 +630,7 @@ It is sensible to combine independent flags with bitwise OR; it is usually nonse ### A default switch case trades exhaustiveness diagnostics for fallback behavior -A \`default\` branch may conceal newly added enumerators from warning configurations, while omitting a fallback can make unnamed values flow past the switch. Use explicit named cases and a deliberate fallback policy chosen for the API. +A `default` branch may conceal newly added enumerators from warning configurations, while omitting a fallback can make unnamed values flow past the switch. Use explicit named cases and a deliberate fallback policy chosen for the API. ### The underlying representation is not a schema @@ -660,10 +660,10 @@ Each failure below is a confusion between two layers of the enum model. | Pitfall | What happens | Do instead | |---|---|---| -| \`static_cast(20)\` where non-fixed range excludes 20 | Undefined behavior | Validate source before cast; use a fixed type where appropriate | -| Assume \`enum class E\` has compiler-chosen underlying type | Contradicts C++11 default of \`int\` | Document default or explicitly select an integral underlying type | +| `static_cast(20)` where non-fixed range excludes 20 | Undefined behavior | Validate source before cast; use a fixed type where appropriate | +| Assume `enum class E` has compiler-chosen underlying type | Contradicts C++11 default of `int` | Document default or explicitly select an integral underlying type | | Assume every enum value has a declared name | Unknown or combined values remain possible | Provide validated parsing and fallback formatting | -| Use \`~flag\` without a mask | Produces bits outside supported flags | Mask to the declared flag universe | +| Use `~flag` without a mask | Produces bits outside supported flags | Mask to the declared flag universe | | Treat a fixed-width enum as a wire layout | Byte order and compatibility remain unspecified | Serialize a specified-width integer explicitly | | Use a default switch branch to silence warnings | Missing newly added enumerators can be overlooked | Enable enum warnings and apply a deliberate fallback strategy | @@ -673,7 +673,7 @@ Each failure below is a confusion between two layers of the enum model. |---|---|---|---| | Enumerators injected into enclosing scope | Yes | Yes | No | | Implicit conversion to integers | Yes | Yes | No | -| Underlying type predetermined by declaration | No | Yes | Yes (\`int\` by default) | +| Underlying type predetermined by declaration | No | Yes | Yes (`int` by default) | | May legally hold unnamed values | Yes, within enum range | Yes, within underlying range | Yes, within underlying range | | Appropriate for flag combinations | Sometimes, with care | Yes, with a flag contract | Yes, with explicit operators | | Can be opaquely declared since C++11 | No (without fixing type) | Yes | Yes | @@ -702,19 +702,19 @@ Compiler diagnostics can detect incomplete switches and accidental enum/integer | Compiler | Settings | Useful coverage | |---|---|---| -| GCC | \`-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wconversion -Wenum-conversion\` | Missing switch enumerators, suspicious conversions and extension use | -| Clang | \`-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wenum-conversion\` | Incomplete switches and enum conversions, with toolchain-specific extra enum warnings | -| MSVC | \`/W4 /permissive- /analyze\` | Common enum switch and conversion diagnostics; consider C4061/C4062 configurations | +| GCC | `-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wconversion -Wenum-conversion` | Missing switch enumerators, suspicious conversions and extension use | +| Clang | `-Wall -Wextra -Wpedantic -Wswitch -Wswitch-enum -Wenum-conversion` | Incomplete switches and enum conversions, with toolchain-specific extra enum warnings | +| MSVC | `/W4 /permissive- /analyze` | Common enum switch and conversion diagnostics; consider C4061/C4062 configurations | -\`-Wswitch-enum\` can reveal omitted named cases even when a \`default\` branch exists, subject to compiler behavior. It cannot prove that every representable unnamed value is handled. +`-Wswitch-enum` can reveal omitted named cases even when a `default` branch exists, subject to compiler behavior. It cannot prove that every representable unnamed value is handled. ### Sanitizers and runtime checks | Tool | Setting | What it contributes | |---|---|---| -| Clang UBSan enum check | \`-fsanitize=enum\` | Detects some loads of enum representations outside a valid range; no business-domain validation | -| Clang/GCC UBSan | \`-fsanitize=undefined\` | Selected undefined behavior paths, with varying instrumentation and limitations | -| Static analysis | clang-tidy, cppcheck, MSVC \`/analyze\` | Suspicious casts, missing cases, and enum/integer API assumptions | +| Clang UBSan enum check | `-fsanitize=enum` | Detects some loads of enum representations outside a valid range; no business-domain validation | +| Clang/GCC UBSan | `-fsanitize=undefined` | Selected undefined behavior paths, with varying instrumentation and limitations | +| Static analysis | clang-tidy, cppcheck, MSVC `/analyze` | Suspicious casts, missing cases, and enum/integer API assumptions | | Protocol tests | Exhaustively test allowed codes, reserved codes, and malformed inputs | Enforces the application-defined accepted domain | A sanitizer may not catch the creation of an invalid enum value at the exact cast site. Do not use runtime instrumentation as an excuse to execute knowingly undefined conversions in example programs. @@ -723,10 +723,10 @@ A sanitizer may not catch the creation of an invalid enum value at the exact cas | Option | Effect | Recommendation | |---|---|---| -| GCC/Clang \`-fshort-enums\` | Can change non-fixed enum representation and ABI | Avoid across independently built binary interfaces | -| \`-std=gnu++20\` versus \`-std=c++20\` | GNU extensions can change which declarations are accepted | Use ISO language modes for portable examples | -| Clang/GCC optimization choices | Can expose reliance on undefined out-of-range enum values | Correct invalid conversions, rather than relying on \`-O0\` observations | -| MSVC \`/permissive-\` | Tightens conformance checking | Use for portable language contracts | +| GCC/Clang `-fshort-enums` | Can change non-fixed enum representation and ABI | Avoid across independently built binary interfaces | +| `-std=gnu++20` versus `-std=c++20` | GNU extensions can change which declarations are accepted | Use ISO language modes for portable examples | +| Clang/GCC optimization choices | Can expose reliance on undefined out-of-range enum values | Correct invalid conversions, rather than relying on `-O0` observations | +| MSVC `/permissive-` | Tightens conformance checking | Use for portable language contracts | These options affect source acceptance and ABI assumptions, not just speed. Review build flags whenever binaries exchange enum-containing values. @@ -734,10 +734,10 @@ These options affect source acceptance and ABI assumptions, not just speed. Revi | Extension | Toolchains | Purpose | Portable alternative | |---|---|---|---| -| \`__PRETTY_FUNCTION__\`-based enum-name extraction | GCC/Clang and similar compiler-specific signatures | Attempts to discover names by instantiating templates | Explicit tables or C++26 reflection where supported | -| \`__FUNCSIG__\`-based extraction | MSVC | Similar compiler-specific name discovery | Explicit tables or C++26 reflection | +| `__PRETTY_FUNCTION__`-based enum-name extraction | GCC/Clang and similar compiler-specific signatures | Attempts to discover names by instantiating templates | Explicit tables or C++26 reflection where supported | +| `__FUNCSIG__`-based extraction | MSVC | Similar compiler-specific name discovery | Explicit tables or C++26 reflection | | Vendor flag-enum attributes | Selected compiler/toolchain environments | Diagnose intended bitmask use | Explicit bitwise operators and documented masks | -| \`-fshort-enums\` | GCC/Clang | Narrow implementation-selected representations | Explicit fixed underlying types at public interfaces | +| `-fshort-enums` | GCC/Clang | Narrow implementation-selected representations | Explicit fixed underlying types at public interfaces | A helper that depends on parsing compiler-provided function-signature strings is not standard enum reflection; isolate it, test exact supported compilers, and never treat it as a portable language guarantee. @@ -764,27 +764,27 @@ Classic enums provide named integral constants with implicit integer conversions ### C++11: Scoped enums and fixed underlying representations -\`enum class\`/\`enum struct\`, explicit underlying types on unscoped enums, opaque declarations, and \`std::underlying_type\` provide stronger type-safety and layout control. An opaque declaration with fixed underlying type yields a complete enum type even before the enumerator list appears. +`enum class`/`enum struct`, explicit underlying types on unscoped enums, opaque declarations, and `std::underlying_type` provide stronger type-safety and layout control. An opaque declaration with fixed underlying type yields a complete enum type even before the enumerator list appears. ### C++14: Alias convenience for underlying-type traits -\`std::underlying_type_t\` reduces template verbosity; no fundamental change to the enum value model occurs. Existing distinction between representation and semantic domain remains. +`std::underlying_type_t` reduces template verbosity; no fundamental change to the enum value model occurs. Existing distinction between representation and semantic domain remains. ### C++17: Direct-list initialization from integers -Single-element direct-list initialization can initialize suitable enums from integers under constrained rules, without creating a general implicit integer conversion. \`std::is_enum_v\` is a convenient variable-template spelling for generic code. +Single-element direct-list initialization can initialize suitable enums from integers under constrained rules, without creating a general implicit integer conversion. `std::is_enum_v` is a convenient variable-template spelling for generic code. ### C++20: Scoped enumerator imports and generic constraints -\`using enum\` introduces enumerator names into a selected scope; concepts make it easier to constrain enum-generic APIs. The old implicit-conversion rules for unscoped enums and the separate notion of unnamed values still apply. +`using enum` introduces enumerator names into a selected scope; concepts make it easier to constrain enum-generic APIs. The old implicit-conversion rules for unscoped enums and the separate notion of unnamed values still apply. ### C++23: Standard underlying-value extraction and scoped-enum traits -\`std::to_underlying\` in \`\` provides a named integer conversion, while \`std::is_scoped_enum\` identifies scoped enumeration types in generic code. Neither offers general enumerator-list introspection. +`std::to_underlying` in `` provides a named integer conversion, while `std::is_scoped_enum` identifies scoped enumeration types in generic code. Neither offers general enumerator-list introspection. ### C++26: Static reflection and enumerator discovery -C++26 static reflection introduces \`\` and \`std::meta::enumerators_of\`, permitting compile-time access to declared enumerators and supporting generation of mapping utilities. This is a genuine advance beyond hand-maintained tables, although real availability requires compiler and standard-library support and reflection does not validate business-domain values. +C++26 static reflection introduces `` and `std::meta::enumerators_of`, permitting compile-time access to declared enumerators and supporting generation of mapping utilities. This is a genuine advance beyond hand-maintained tables, although real availability requires compiler and standard-library support and reflection does not validate business-domain values. When discussing support, name the precise standard and the feature-test conditions used by the example instead of assuming every compiler labeled C++26 has the full library. @@ -798,22 +798,22 @@ Languages with managed enum-like classes often provide runtime access to named m | Concept | Java | Python | C# | C++ reality | |---|---|---|---|---| -| Enumeration model | Class-based enum constants | \`Enum\` / \`IntEnum\` objects | Value-type \`enum\` with fixed underlying integer | Distinct enum type with scoped or unscoped enumerators | -| Unnamed integer value | Not generally a Java enum instance | \`Enum\` construction typically rejects unknown values | Explicit casts can produce unnamed values | Fixed-underlying enum can represent unnamed values | -| Listing members | \`values()\`/reflection | Iteration and \`__members__\` | \`Enum.GetValues\` | No portable built-in listing before C++26 reflection | -| Integer conversion | Explicit numeric fields by design | \`IntEnum\` integrates with int | Explicit enum/numeric conversion | Implicit for unscoped; explicit for scoped | -| Invalid range | Managed validation and exceptions | Exceptions for unrecognized \`Enum\` values | Underlying integral value can be cast | Out-of-range conversion to non-fixed enums may have UB | +| Enumeration model | Class-based enum constants | `Enum` / `IntEnum` objects | Value-type `enum` with fixed underlying integer | Distinct enum type with scoped or unscoped enumerators | +| Unnamed integer value | Not generally a Java enum instance | `Enum` construction typically rejects unknown values | Explicit casts can produce unnamed values | Fixed-underlying enum can represent unnamed values | +| Listing members | `values()`/reflection | Iteration and `__members__` | `Enum.GetValues` | No portable built-in listing before C++26 reflection | +| Integer conversion | Explicit numeric fields by design | `IntEnum` integrates with int | Explicit enum/numeric conversion | Implicit for unscoped; explicit for scoped | +| Invalid range | Managed validation and exceptions | Exceptions for unrecognized `Enum` values | Underlying integral value can be cast | Out-of-range conversion to non-fixed enums may have UB | ### Common wrong assumptions -An enum class in Java behaves like a closed collection of singleton objects; a C++ \`enum class\` does not guarantee that every representable value has a named enumerator. A Python \`Enum\` constructor often raises for an unknown integer; the corresponding C++ cast may produce an unnamed value or even undefined behavior for a non-fixed enum. C# programmers will recognize unnamed enum values but must still account for C++'s distinct range rules and ABI. +An enum class in Java behaves like a closed collection of singleton objects; a C++ `enum class` does not guarantee that every representable value has a named enumerator. A Python `Enum` constructor often raises for an unknown integer; the corresponding C++ cast may produce an unnamed value or even undefined behavior for a non-fixed enum. C# programmers will recognize unnamed enum values but must still account for C++'s distinct range rules and ABI. ### Idiomatic C++ replacement | Managed-language habit | Idiomatic C++ | |---|---| | Query names/values automatically | Maintain a constexpr table, use a tested library, or C++26 reflection | -| Rely on constructor rejection of unknown codes | Parse integers explicitly and return \`std::optional\` or an error result | +| Rely on constructor rejection of unknown codes | Parse integers explicitly and return `std::optional` or an error result | | Combine flags through general integer operators | Define constrained typed flag operations and a valid mask | | Persist ordinal positions | Use explicit stable underlying values and a defined byte serialization | | Depend on exhaustive enum dispatch | Use warning-backed switches with a deliberate unknown-value fallback | @@ -829,11 +829,11 @@ These references are useful for checking standard wording on ranges, conversions ### Standard and language reference * [Enumeration declarations and conversions](https://en.cppreference.com/w/cpp/language/enum) -* [Explicit \`static_cast\` to an enum](https://en.cppreference.com/w/cpp/language/static_cast) -* [\`std::underlying_type\`](https://en.cppreference.com/w/cpp/types/underlying_type) -* [\`std::to_underlying\` (C++23)](https://en.cppreference.com/w/cpp/utility/to_underlying) -* [\`std::is_scoped_enum\` (C++23)](https://en.cppreference.com/w/cpp/types/is_scoped_enum) -* [\`std::meta::enumerators_of\` (C++26)](https://en.cppreference.com/w/cpp/meta/enumerators_of) +* [Explicit `static_cast` to an enum](https://en.cppreference.com/w/cpp/language/static_cast) +* [`std::underlying_type`](https://en.cppreference.com/w/cpp/types/underlying_type) +* [`std::to_underlying` (C++23)](https://en.cppreference.com/w/cpp/utility/to_underlying) +* [`std::is_scoped_enum` (C++23)](https://en.cppreference.com/w/cpp/types/is_scoped_enum) +* [`std::meta::enumerators_of` (C++26)](https://en.cppreference.com/w/cpp/meta/enumerators_of) ### Proposals and guidelines