<< Home

Chapter 15: Compile Time Programming

Compile time programming lets the compiler do real work before your program ever runs. You can calculate values, shape types, and even remove entire branches of code using constant evaluation. The result is smaller binaries, faster execution, and clearer intent; you tell the compiler what must be known early, and it verifies your claim.

constexpr and constant evaluation

constexpr marks expressions and entities that can be evaluated at compile time when given constant inputs. A constexpr function may still run at runtime if called with nonconstant arguments; the keyword grants permission for constant evaluation, and the compiler decides based on the call site. Constant evaluation applies to literals, pure arithmetic, suitable library calls, and well formed constexpr functions; the compiler then folds results directly into your program image.

πŸ’‘ If a constexpr function is called with constant arguments, the result becomes a constant expression and can be used where an integral constant is required; for example an array bound or a template parameter.

Rules for constexpr functions

A constexpr function must have a body that the compiler can analyze without side effects; it can read literal types and call other constexpr functions, but it may not perform I/O or mutate nonlocal state. In practice this means most arithmetic and many algorithms over simple aggregates can be lifted to compile time.

constexpr int gcd(int a, int b) {
  while (b != 0) {
    int t = a % b;
    a = b;
    b = t;
  }
  return a;
}
static_assert(gcd(48, 18) == 6);

Variables, constructors, and constexpr objects

Objects can be declared constexpr when their initialization is a constant expression. Types with constexpr constructors allow you to build richer constant objects, such as fixed lookup tables or geometry primitives. If any subexpression is not constant, the variable silently falls back to runtime initialization.

struct Point {
  int x;
  int y;
  constexpr int manhattan() const { return x + y; }
};
constexpr Point p{3, 4};
static_assert(p.manhattan() == 7);

consteval and immediate functions

consteval declares an immediate function; every call must be evaluated during compilation. If a call happens in a context that is not constant, the program is ill formed. Use immediate functions when a value must exist before code generation, such as computing sizes for static arrays or validating format strings.

When to prefer consteval over constexpr

Choose consteval when runtime fallback would hide a bug. If a function encodes a contract that only makes sense at compile time, require it. Otherwise keep constexpr for flexibility. This policy keeps diagnostics sharp; mistakes surface where the bad call occurs.

consteval int index_of(char c) {
  if (c < 'a' || c > 'z') {
    throw "only lowercase a..z"; // triggers a compile time error
  }
  return c - 'a';
}
constexpr int i = index_of('c'); // ok
// int j = index_of(std::getchar()); // ill formed
⚠️ Throwing in immediate functions is only for signaling failure during constant evaluation; it does not create a runtime exception path.

Type traits and detection idioms

Type traits expose compile time facts about types, such as whether a type is trivially copyable or whether an expression is valid. The standard library provides a rich set in <type_traits>; you combine them with boolean logic or use them as predicates in templates. Detection idioms let you probe for the presence of members, aliases, or operations without forming ill formed expressions.

Core traits and boolean plumbing

Most traits follow the pattern std::is_…<T>::value and an alias form std::is_…_v<T>. To transform types there are helpers like std::remove_reference_t<T> and std::decay_t<T>. These tools let you write templates that adapt to arguments and still remain readable.

TraitMeaningExample
std::is_integral_v<T>True for integral typesstd::is_integral_v<int>
std::is_same_v<A,B>Type equality checkstd::is_same_v<int, const int>
std::is_trivially_copyable_v<T>Memcpy safe copystd::is_trivially_copyable_v<std::byte>
template<class T>
constexpr bool is_small_trivial_v =
  sizeof(T) <= sizeof(void*) && std::is_trivially_copyable_v<T>;

static_assert(is_small_trivial_v<int>);

The classic detection idiom

The detection idiom creates a dependent context that is valid when an expression is well formed. The simplest form uses std::void_t with a primary template and a partial specialization. If substitution fails, the specialization is discarded and the primary template remains.

template<class, class = void>
struct has_reserve : std::false_type {};

template<class T>
struct has_reserve<T, std::void_t<decltype(std::declval<T&>().reserve(0))>>
  : std::true_type {};

static_assert(has_reserve<std::vector<int>>::value);

Detection utilities

Modern C++ lets you detect expressions more directly with requires. The expression form yields a boolean at compile time and reads like a miniature interface declaration. It works well with concepts and simplifies older SFINAE patterns.

template<class T>
concept Reservable = requires(T t, std::size_t n) {
  { t.reserve(n) } -> std::same_as<void>;
};

static_assert(Reservable<std::vector<int>>);

Metafunctions and compile time containers

A metafunction is a template that maps types or values to new types or values. They are the building blocks of template metaprogramming; you use them to select overloads, compute constants, or generate structures. Compile time containers hold values in the type system or as constant data; they enable unrolled loops and table driven code generation.

Type to value and value to type transformations

Metafunctions often expose a nested ::type or an alias template for clarity. For values, use std::integral_constant and its aliases such as std::bool_constant. Compose these pieces to build readable compile time logic.

template<class T> struct add_ptr { using type = T*; };
template<class T> using add_ptr_t = typename add_ptr<T>::type;

static_assert(std::is_same_v<add_ptr_t<int>, int*>);

template<int N> using int_c = std::integral_constant<int, N>;
static_assert(int_c<42>::value == 42);

std::integer_sequence and index packs

std::integer_sequence and its helper std::make_index_sequence present a sequence of compile time integers. You use them to expand parameter packs, to unroll operations across tuples, and to synthesize arrays without loops in the runtime sense.

template<class Tuple, std::size_t... I>
constexpr auto tuple_to_array_impl(const Tuple& t, std::index_sequence<I...>) {
  return std::array{std::get<I>(t)...};
}

template<class... Ts>
constexpr auto tuple_to_array(const std::tuple<Ts...>& t) {
  return tuple_to_array_impl(t, std::make_index_sequence<sizeof...(Ts)>{});
}

static_assert(tuple_to_array(std::tuple{1,2,3})[1] == 2);
πŸ’‘ Index sequences let you write a single source of truth; the pack drives both compile time shapes and the generated statements.

Building constant tables

You can fill std::array or custom structs in a constexpr context and then reference them at runtime without overhead. The compiler stores the final bytes directly in the data segment or even folds them into instruction immediates.

constexpr auto make_squares() {
  std::array<int, 8> a{};
  for (std::size_t i = 0; i < a.size(); ++i) a[i] = static_cast<int>(i * i);
  return a;
}
constexpr auto squares = make_squares();
static_assert(squares[3] == 9);

Generating efficient code at build time

Compile time programming is not only about cleverness; it is about guiding the compiler to form the best machine code. Use if constexpr to erase dead branches, compute shapes and sizes up front, and prebuild tables that replace runtime computation. The payoff is clear assembly and predictable performance.

Erasing branches

if constexpr evaluates its condition during compilation; the false branch is discarded and never instantiated. This removes unused code paths and avoids hard to read SFINAE tricks. The pattern pairs well with concepts and traits.

template<class T>
void serialize(const T& t) {
  if constexpr (std::is_integral_v<T>) {
    // write integer fast path
  } else if constexpr (std::is_floating_point_v<T>) {
    // write float path
  } else {
    // generic reflection path …
  }
}

Table driven computation

When inputs come from small finite sets, compute results ahead of time and index into a constant table. This replaces branches and arithmetic with a single load. Hash seeds, bit counts, and small transforms are classic candidates.

constexpr std::array<unsigned char, 256> popcount_lut = []{
  std::array<unsigned char, 256> a{};
  for (int i = 0; i < 256; ++i) {
    unsigned x = static_cast<unsigned>(i);
    unsigned c = 0;
    while (x) { c += x & 1u; x >>= 1; }
    a[static_cast<std::size_t>(i)] = static_cast<unsigned char>(c);
  }
  return a;
}();

inline unsigned popcount8(unsigned char x) {
  return popcount_lut[x];
}
⚠️ Precomputing tables increases binary size; prefer small domains or compress the data when the table is large.

Validating inputs during compilation

Immediate functions and static_assert let you check invariants before a program can be built. You can validate parseable string literals, format patterns, and bitfield layouts. The resulting diagnostics point at the call sites and speed up debugging.

consteval bool valid_hex_digit(char c) {
  return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}

template<std::size_t N>
consteval void check_hex_literal(const char(&s)[N]) {
  for (std::size_t i = 0; i + 2 < N; ++i) {
    if (s[i] == '0' && s[i+1] == 'x') {
      for (std::size_t j = i + 2; j + 1 < N; ++j) {
        if (!valid_hex_digit(s[j])) throw "bad hex digit";
      }
    }
  }
}

constexpr const char* id = "0x12af";
consteval auto _ = (check_hex_literal(id), 0);

Putting it together

The most effective pattern blends concepts, detection, if constexpr, and constant data. Detect capabilities, select fast paths, and bake shapes or tables at build time. The runtime code stays small and the compiler has maximum room to optimize away the scaffolding.