C++ Literal Types and Compile-Time Constraints
In modern C++, literal types form the foundational category of data
types that can be created, manipulated, and evaluated at compile time.
This article breaks down what defines a literal type under the C++
standard, examines the structural requirements for classes to qualify as
literal types, and explores the operational constraints they face during
constant evaluation in constexpr and consteval
contexts.
What Is a Literal Type?
A literal type is any type whose layout and state can be fully
determined and handled by the compiler during translation. Literal types
are the only types that can be used to declare constexpr
variables, serve as non-type template parameters (NTTPs), or be passed
to and returned from constexpr functions evaluated at
compile time.
According to the C++ standard, a type is a literal type if it belongs to one of the following categories:
- Scalar types: Arithmetic types (integers,
floating-point numbers), enumerations, pointer types, pointer-to-member
types, and
std::nullptr_t. - Reference types: Any lvalue or rvalue reference to a literal type.
- Array types: An array whose element type is a literal type.
- Literal class types: Classes, structs, or unions that satisfy specific structural constraints.
void: (in certain contexts such as return types ofconstexprfunctions).
Structural Requirements for Literal Classes
For a user-defined class, struct, or union to be recognized as a literal type, it must meet several strict structural prerequisites:
- Destructor: It must have a trivial destructor, or,
since C++20, a
constexprdestructor. - Constructors: It must be an aggregate type, or it
must possess at least one
constexprconstructor that is not a copy or move constructor. - Members and Base Classes: All non-static data members and base classes must be non-volatile literal types.
- Union Membership: If it is a union, it must have at least one non-static data member of non-volatile literal type. If it is a non-union class containing an anonymous union, that union's members must also be non-volatile literal types.
Compile-Time Constraints
While literal types enable powerful compile-time computations, their
use within constant expressions (constexpr,
consteval, and NTTPs) is strictly bounded by language
constraints to ensure deterministic, reproducible builds without side
effects on the host compiler.
1. Absence of Undefined Behavior
Any operation that would cause undefined behavior (UB) at runtime immediately halts compilation with an error. This includes:
- Accessing uninitialized memory or out-of-bounds array indices.
- Signed integer overflow.
- Null pointer dereferences.
- Division by zero.
2. Restrictions on Type Casting
Type punning and non-type-safe conversions are prohibited during
compile-time evaluation. reinterpret_cast is strictly
forbidden. C-style casts that resolve to a reinterpret_cast
are also illegal. Downcasting via static_cast or
dynamic_cast is limited to valid polymorphic hierarchies
(supported since C++20) where the complete dynamic type is known.
3. Transient Dynamic Allocation
Since C++20, dynamic memory allocation (new and
delete, along with containers like std::vector
and std::string) is permitted in compile-time contexts.
However, all allocated memory must be deallocated before the constant
expression finishes evaluation. Memory cannot leak into the runtime
binary; non-transient allocations cause a compilation failure.
4. I/O and External State
Compile-time execution cannot interact with the operating system or host environment. File input/output, network sockets, system calls, inline assembly, and volatile memory access are strictly prohibited within literal evaluations.
5. Concurrency and Synchronization
Literal evaluation is single-threaded from the perspective of the
language semantics. Spawning threads, using synchronization primitives
(std::mutex, std::atomic), or invoking
thread-local variables that are not initialized at compile time are
disallowed.