Underlying Type Rules for C++ Unscoped Enums
In the C++ programming language, unscoped enumerations
(enum) are backed by an integral type that dictates their
memory footprint, signedness, and valid value range. While modern C++
allows developers to explicitly specify this underlying type, omitting
it leaves the selection to implementation-defined rules constrained by
the C++ standard. This article details how compilers determine the
underlying integral representation for unscoped enumerations,
distinguishing between fixed and non-fixed underlying types and
detailing the exact limits imposed by the language specification.
Explicit Underlying Type (Fixed)
Since C++11, unscoped enumerations can specify an explicit underlying type using a colon followed by an integral type name.
enum Status : unsigned char {
Pending,
Approved,
Rejected
};When an explicit type is defined, the underlying representation is fixed to that exact type. It follows that type's size, signedness, alignment, and range, regardless of the values of the enumerators defined within the block.
Implicit Underlying Type (Non-Fixed)
When an unscoped enumeration does not specify an underlying type, the compiler determines it based on rules defined in the ISO C++ standard:
- Capacity to Hold All Values: The underlying type must be an integral type capable of representing every enumerator value defined within the enumeration. If no standard or extended integral type can represent all values, the program is ill-formed.
- Size Constraint Relative to
int: The underlying type is implementation-defined, but it cannot be larger thanintunless the value of an enumerator exceeds the range of bothintandunsigned int. - Handling of Larger Values: If an enumerator value
cannot fit into
intorunsigned int, the compiler chooses a larger integral type, such aslong,unsigned long,long long, orunsigned long long, that can hold all values. - Signedness: If all enumerators are non-negative,
the compiler may select either a signed or an unsigned integral type
(typically
intorunsigned int). If any enumerator is negative, the underlying type must be signed. - Empty Enumerations: If the enumerator list is empty
(
enum Empty {};), the underlying type is chosen as an integral type capable of representing the value0(oftenint).
Range of Values
The valid values an unscoped enumeration can store depend directly on whether the underlying type is fixed or non-fixed:
- Fixed underlying type: The range of valid values is
identical to the range of the chosen integral type. Any value that fits
within that type can be cast to the enumeration using
static_cast. - Non-fixed underlying type: The range of valid values is bounded by the smallest bitfield large enough to hold all enumerator values. If the smallest enumerator is non-negative, the range is \([0, 2^M - 1]\), where \(M\) is the number of bits needed to represent the largest enumerator. If there are negative values, two's complement representation is used, spanning \([-(2^{M-1}), 2^{M-1} - 1]\). Assigning a value outside this computed range via explicit conversion results in undefined behavior.
Inspecting the Type Programmatically
You can inspect the chosen integral representation in code using the
standard library trait std::underlying_type:
#include <type_traits>
enum LegacyFlag {
FlagA = 1,
FlagB = 2
};
using FlagType = std::underlying_type<LegacyFlag>::type;This trait yields the exact integral type selected by the compiler for that enumeration.