Compile-Time Programming

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.

constexpr/consteval/constinit were introduced in Constants, Enumerations, and Initialization; this page covers the broader toolkit for computing and branching at compile time.

static_assert

A compile-time assertion — fails the build, with a message, instead of failing at run time:

template <typename T>
struct Vector3 {
    static_assert(std::is_arithmetic_v<T>, "Vector3 requires an arithmetic component type");
    T x, y, z;
};

static_assert(sizeof(int) == 4, "this code assumes a 32-bit int");

<type_traits>: Querying and Writing Traits

The standard library’s compile-time type-introspection toolkit — used constantly inside generic/template code to branch on a type’s properties:

#include <type_traits>
#include <vector>

struct Base {};
struct Derived : Base {};

static_assert(std::is_integral_v<int>);
static_assert(!std::is_integral_v<double>);
static_assert(std::is_same_v<int, int>);
static_assert(std::is_pointer_v<int*>);
static_assert(std::is_base_of_v<Base, Derived>);

// Writing a custom trait: detect whether T has a "size()" member
template <typename T, typename = void>
struct HasSize : std::false_type {};

template <typename T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};

template <typename T>
inline constexpr bool HasSizeV = HasSize<T>::value;

static_assert(HasSizeV<std::vector<int>>);
static_assert(!HasSizeV<int>);

std::void_t plus a partial specialization is the classic "detection idiom" — since C++20, a requires expression (see Concepts and Constraints) expresses the same check far more readably: template <typename T> concept HasSize = requires(T t) { t.size(); };.

std::conditional and std::enable_if vs. Concepts

#include <type_traits>

template <typename T>
using StorageType = std::conditional_t<sizeof(T) <= 8, T, T&>;   // pick T or T& based on its size

template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> onlyIntegral(T value) {   // SFINAE: this overload only
    return value;                                                     // participates for integral T
}

std::enable_if/SFINAE was the pre-C++20 way to constrain a template — it works, but produces poor error messages and clutters signatures. Concepts (requires Numeric<T> from Concepts and Constraints) express the same constraint more directly and are strongly preferred in new C++20/23 code; enable_if remains common in pre-C++20 codebases and libraries that still support C++17.

if constexpr and std::is_constant_evaluated

if constexpr was introduced in Control Flow. C++23’s if consteval (also covered there) is generally preferred over the older std::is_constant_evaluated() function, but the latter still appears in code that must also support C++20:

#include <type_traits>

constexpr double approxSqrtOld(double x) {
    if (std::is_constant_evaluated()) {          // C++20 way to ask "am I running at compile time?"
        double guess = x / 2 + 1;
        for (int i = 0; i < 20; ++i) guess = (guess + x / guess) / 2;
        return guess;
    } else {
        return __builtin_sqrt(x);
    }
}

if consteval { …​ } (C++23) is preferred over if (std::is_constant_evaluated()) because it cannot be accidentally combined with other runtime conditions in a way that breaks the compile-time guarantee.

Constexpr Virtual Functions

C++20 allows virtual functions to be constexpr, so polymorphic code can run at compile time as long as the dynamic type is itself known at compile time:

struct Shape {
    constexpr virtual double area() const { return 0.0; }
    constexpr virtual ~Shape() = default;
};
struct Square : Shape {
    constexpr explicit Square(double side) : side_(side) {}
    constexpr double area() const override { return side_ * side_; }
private:
    double side_;
};

constexpr double computeArea() {
    Square s(4.0);              // the dynamic type is statically known here -- eligible for compile-time evaluation
    return s.area();
}
static_assert(computeArea() == 16.0);

See Also

  • C: Type-Generic Programming — C23 has constexpr for objects only — no consteval, no <type_traits>, and _Generic in place of if constexpr.