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 of constexpr functions).

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:

  1. Destructor: It must have a trivial destructor, or, since C++20, a constexpr destructor.
  2. Constructors: It must be an aggregate type, or it must possess at least one constexpr constructor that is not a copy or move constructor.
  3. Members and Base Classes: All non-static data members and base classes must be non-volatile literal types.
  4. 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.