Constants, Enumerations, and Initialization

This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com.

This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

const, constexpr, consteval, constinit

const int limit = 100;             // run-time constant: cannot be reassigned, but value need not be known at compile time
constexpr int square(int x) {      // may run at compile time OR run time, chosen by the compiler
    return x * x;
}
constexpr int nine = square(3);     // guaranteed evaluated at compile time (used in a constant expression)

consteval int mustBeCompileTime(int x) {   // C++20: an "immediate function" -- MUST run at compile time
    return x * x;
}
// int bad = mustBeCompileTime(getRuntimeValue());   // error: argument not a constant expression

constinit int initializedOnce = square(4);  // C++20: guarantees compile-time *initialization*, but the
                                             // variable itself may still be mutated later at run time -- unlike
                                             // constexpr, constinit does not imply const

constinit solves the "static initialization order fiasco" for namespace-scope variables that must not run a dynamic initializer at process startup, without forcing them to also be immutable afterward.

Scoped and Unscoped Enumerations

#include <utility>   // std::to_underlying (C++23)
#include <iostream>

enum Color { Red, Green, Blue };            // unscoped: enumerators leak into the surrounding scope,
                                             // implicitly convert to int
enum class Suit { Clubs, Diamonds, Hearts, Spades };   // scoped: enumerators must be qualified, no implicit
                                                        // conversion to int

int main() {
    Color c = Red;                          // fine, unqualified
    Suit s = Suit::Hearts;                  // must qualify

    int underlying = std::to_underlying(s);  // C++23: replaces static_cast<int>(s), and works generically
    std::cout << underlying << '\n';         // 2

    using enum Suit;                        // C++20: brings Suit's enumerators into scope, unqualified
    Suit s2 = Hearts;                        // now legal without "Suit::"
    (void)c; (void)s2;
}

Prefer enum class in new code — unscoped enum is kept mainly for interop with older code and C headers.

Uniform/List Initialization and Its Pitfalls

Brace initialization ({}) works uniformly across variables, aggregates, and containers, and rejects narrowing conversions that ()/= silently allow:

int a{5};                 // direct-list-initialization
int b = {5};               // copy-list-initialization
// int c{5.5};             // error: narrowing conversion from double to int -- caught at compile time
int d(5.5);                 // allowed, and silently truncates to 5 -- the pitfall {} avoids

std::vector<int> v1{1, 2, 3};      // three elements: 1, 2, 3
std::vector<int> v2(3, 7);          // three elements, all 7 -- the "most vexing parse"-adjacent trap: () here
                                     // picks the (count, value) constructor, not a 3-element list

The classic pitfall: std::vector<int> v{3, 7} (braces) means two elements, 3 and 7, while std::vector<int> v(3, 7) (parens) means three elements, each 7 — braces prefer an initializer_list constructor when one exists, parens never do.

Default Member Initializers

struct Config {
    int retries = 3;              // used whenever a constructor doesn't set retries itself
    bool verbose = false;
    std::string name{"default"};
};

Config c1;                        // retries=3, verbose=false, name="default"
Config c2{.retries = 5};           // designated initializer overrides just retries; see below

Aggregate Initialization and Designated Initializers

An aggregate (no user-declared constructors, no private/protected non-static data members, no virtual functions, no base classes other than public non-virtual ones) can be initialized member-by-member with braces, optionally naming each member (C++20 designated initializers):

struct Point3D {
    double x;
    double y;
    double z = 0.0;
};

Point3D p1{1.0, 2.0, 3.0};             // positional
Point3D p2{.x = 1.0, .y = 2.0};         // designated -- z keeps its default member initializer, 0.0
// Point3D p3{.y = 2.0, .x = 1.0};      // error: designators must appear in declaration order

See Also