C++ constexpr Functions and Variables: Compute at Compile Time [#26-1]

C++ constexpr Functions and Variables: Compute at Compile Time [#26-1]

이 글의 핵심

Practical guide to constexpr variables and functions: compile-time evaluation, array sizes, template non-type arguments, CRC/hash tables, and production patterns.

Introduction: “I want this computed at compile time”

Problem scenarios

When building C++ projects you often hit:

  • You need an array size from a runtime variable and int arr[n] fails to compile.
  • CRC32, hash tables, or similar values are recomputed every run even though they are fixed.
  • You want configuration (buffer sizes, timeouts) computed once at build time, not hard-coded magic numbers.
  • You need template arguments like std::array<int, 128> but cannot use getBufferSize() unless it is a constant expression.

More scenarios

Scenario 1: Fixed protocol buffer size
Packet header is always 16 bytes and max payload 4096; you want std::array<uint8_t, 16 + 4096> with the size known at compile time—constexpr keeps this maintainable without macros.

Scenario 2: Base64 lookup table
The 64-character table never changes at runtime; building it with constexpr removes initialization cost and places data in .rodata.

Scenario 3: JSON schema limits
Max fields and string lengths defined in a schema can be turned into compile-time constants for std::array sizes and static_assert.

Scenario 4: Embedded memory map
Register offsets and region sizes per board can be constexpr switch cases or template parameters.

Scenario 5: Deterministic unit tests
Fixed seeds for reproducible random sequences with zero runtime seed computation.

In C++, array sizes, template non-type parameters, switch cases, etc., require constant expressions—values fixed at compile time. Runtime values cannot be used in those positions.

constexpr marks functions and variables that can be evaluated at compile time, so you can use their results wherever a constant expression is required. From C++14 onward, loops and multiple statements are allowed; C++20 extends constexpr further across the standard library.

Goals

  • constexpr variables and constexpr functions
  • Relaxed rules in C++14/20
  • Overview of if constexpr
  • Typical errors and production patterns

When constexpr is evaluated

flowchart TD
    A[constexpr function call] --> B{Arguments constant expressions?}
    B -->|Yes| C[Evaluated at compile time]
    B -->|No| D[Evaluated at runtime]
    C --> E[Usable for array sizes, template args, etc.]
    D --> F[Behaves like a normal function]

constexpr vs macros

flowchart LR
    subgraph macro["Macros"]
        M1[Preprocessor substitution] --> M2[No type checking]
        M2 --> M3[Hard to debug]
    end
    subgraph constexpr["constexpr"]
        C1[Compiler evaluation] --> C2[Type checking]
        C2 --> C3[Namespaces and scope]
        C3 --> C4[Overloading and templates]
    end

After reading this post

  • Define compile-time constants with constexpr.
  • Understand constexpr function constraints and extensions.
  • Use results for array sizes and template arguments.
  • Distinguish const vs constexpr.
  • Apply patterns (lookup tables, config parsing).

Real-world pain

Lessons from applying these ideas in larger projects.

Situation and fix

Theory from books often diverges from real code. Code review and profiling surfaced inefficiencies; hands-on iteration mattered.


Table of contents

  1. constexpr variables
  2. constexpr functions in detail
  3. constexpr variables and constructors
  4. constexpr vs const
  5. C++14/20 extensions
  6. Common errors
  7. Performance benchmarks
  8. Production patterns
  9. Practical use

1. constexpr variables

Compile-time constants

A constexpr variable must have its value fixed at compile time. You can use it for raw array sizes and std::array template arguments. A const variable initialized at runtime is read-only but not a constant expression for array bounds.

constexpr int MAX = 100;
constexpr double PI = 3.14159265358979;

int arr[MAX];  // OK: constant expression
std::array<int, MAX> a;  // OK

Initialization rules

constexpr variables must be initialized from literals or other constexpr values—not from arbitrary runtime calls.

constexpr int a = 42;           // OK: literal
constexpr int b = a + 1;        // OK: another constexpr
constexpr int c = add(2, 3);    // OK if add is constexpr and args are constant

int runtime_val = getValue();
constexpr int d = runtime_val;  // Compile error!

2. constexpr functions

Basics

A constexpr function can be invoked at compile time when its arguments are constant expressions. add(3, 5) folds to 8 at compile time; you can still call add(i, j) at runtime with variable arguments.

#include <iostream>

constexpr int add(int a, int b) {
    return a + b;
}

int main() {
    constexpr int x = add(3, 5);  // 8 at compile time
    std::cout << x << "\n";
    return 0;
}

Output: a single line printing 8.

Example 1: factorial (C++14)

C++14 allows if, multiple return statements, and recursion in constexpr functions.

constexpr unsigned long long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main() {
    constexpr auto f5 = factorial(5);  // 120 at compile time
    std::array<int, factorial(5)> arr; // size 120
    return 0;
}

Example 2: Fibonacci

constexpr functions are often easier to read than recursive templates.

constexpr int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

template <int N>
struct Fib {
    static constexpr int value = fib(N);
};

int main() {
    constexpr int f10 = fib(10);  // 55
    constexpr int f20 = Fib<20>::value;  // 6765
    return 0;
}

Example 3: next power of two (loop)

constexpr size_t nextPowerOfTwo(size_t n) {
    size_t p = 1;
    while (p < n) p *= 2;
    return p;
}

std::array<int, nextPowerOfTwo(100)> buf;  // size 128

Example 4: string length (C++14)

constexpr size_t strLen(const char* s) {
    size_t len = 0;
    while (s[len] != '\0') ++len;
    return len;
}

constexpr size_t LEN = strLen("hello");  // 5
std::array<char, strLen("hello") + 1> buf;

Example 5: conditional clamp

constexpr int clamp(int value, int min_val, int max_val) {
    if (value < min_val) return min_val;
    if (value > max_val) return max_val;
    return value;
}

constexpr int c = clamp(150, 0, 100);  // 100

Example 6: CRC32 lookup table (full example)

#include <array>
#include <cstdint>
#include <utility>

constexpr uint32_t crc32_table_entry(uint32_t idx) {
    uint32_t crc = idx;
    for (int i = 0; i < 8; ++i) {
        crc = (crc >> 1) ^ (0xEDB88320u & -(crc & 1));
    }
    return crc;
}

template <size_t... Is>
constexpr auto make_crc32_table(std::index_sequence<Is...>) {
    return std::array<uint32_t, sizeof...(Is)>{{crc32_table_entry(static_cast<uint32_t>(Is))...}};
}

constexpr auto CRC32_TABLE = make_crc32_table(std::make_index_sequence<256>{});

uint32_t crc32(const uint8_t* data, size_t len) {
    uint32_t crc = 0xFFFFFFFFu;
    for (size_t i = 0; i < len; ++i) {
        crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    return crc ^ 0xFFFFFFFFu;
}

Benefit: CRC32_TABLE is built once at compile time and lives in .rodata.

Example 7: compile-time string hash for switch

constexpr unsigned long long hash_fnv1a(const char* str) {
    unsigned long long hash = 14695981039346656037ULL;
    while (*str) {
        hash ^= static_cast<unsigned long long>(*str++);
        hash *= 1099511628211ULL;
    }
    return hash;
}

constexpr auto CMD_HASH = hash_fnv1a("start");

void handle_command(const char* cmd) {
    switch (hash_fnv1a(cmd)) {
        case CMD_HASH:
            break;
        default:
            break;
    }
}

3. constexpr variables and constructors

Literal types

constexpr variables require literal types: scalars, arrays, and user-defined types with constexpr constructors.

constexpr constructor example

struct Point {
    int x, y;
    constexpr Point(int x, int y) : x(x), y(y) {}
    constexpr int sum() const { return x + y; }
};

constexpr Point p(1, 2);
constexpr int s = p.sum();  // 3

Constraints

  • Constructor body must only call other constexpr-friendly operations.
  • All members must be constexpr-initializable.
  • No virtual inheritance or virtual calls (pre-C++20).
struct Vec3 {
    float x, y, z;
    constexpr Vec3(float x, float y, float z) : x(x), y(y), z(z) {}
    constexpr float lengthSq() const {
        return x * x + y * y + z * z;
    }
};

constexpr Vec3 v(1.0f, 0.0f, 0.0f);
constexpr float lenSq = v.lengthSq();  // 1.0

4. constexpr vs const

Aspectconstconstexpr
MeaningNot modified after initComputable at compile time
InitMay be runtimeMust be compile time
Constant expressionNo (if runtime init)Yes
Array boundsOnly if compile-time initYes
int getValue() { return 42; }

void example() {
    const int a = 10;
    const int b = getValue();   // OK at runtime

    int arr1[a];                // OK: a is a compile-time constant
    int arr2[b];                // Error: b is runtime

    constexpr int c = 10;
    // constexpr int d = getValue(); // error

    std::array<int, c> arr3;    // OK
}

Use constexpr when you need constant expressions (sizes, templates, static_assert). Use const for “immutable after first assignment” at runtime.


5. C++14/20 extensions

C++14

  • Multiple returns, loops, local variables in constexpr functions.
  • More constexpr member functions.

C++20

  • More operations allowed (including dynamic allocation in some contexts).
  • consteval: must be compile-time only.
  • Much of the standard library becomes constexpr-friendly.

if constexpr

Only the true branch is instantiated when the condition is a compile-time constant—handy for templates.

template <typename T>
auto unwrap(T x) {
    if constexpr (std::is_pointer_v<T>)
        return *x;
    else
        return x;
}

6. Common errors and fixes

Error 1: Non-literal types in constexpr

Cause: constexpr values/functions only deal with literal types. std::string and std::vector were not literal types before C++20.

// Error in C++17 and below
constexpr std::string msg = "hello";

// C++20: std::string can be constexpr in some contexts
// C++17 and below: fixed-size array
constexpr char msg[] = "hello";

Error 2: Runtime-only operations

Cause: In C++14 and below, dynamic allocation, exceptions, and virtual calls inside constexpr were restricted.

constexpr int bad() {
    int* p = new int(42);
    return *p;
}

constexpr int good(int x) {
    return x * 2;
}

Error 3: Non-constant arguments to constexpr functions

Cause: If arguments are not constant expressions, the function may run at runtime—that is intended. Using the result where a constant expression is required then fails.

constexpr int add(int a, int b) { return a + b; }

int main() {
    int x = 3, y = 5;
    // constexpr int z = add(x, y);  // error
    int w = add(x, y);
    constexpr int v = add(3, 5);
    return 0;
}

Error 4: Recursion depth exceeded

Use an iterative version or a lookup table instead of very deep recursive constexpr.

Error 5: Returning pointers/references to locals

In C++14 and below, returning addresses of locals from constexpr functions is problematic—prefer returning by value.

Error 6: static_assert with runtime values

Use templates or constexpr validation functions; static_assert needs a compile-time boolean.

Error 7: std::array size not a constant expression

get_size() must be constexpr to use std::array<int, get_size()>.

Error 8: Language version mismatch

C++11 constexpr bodies were very restricted; C++11-compatible factorial uses a single return with recursion instead of a loop.

Error 9: I/O or global state in constexpr

No std::cout, file I/O, or mutating globals during constexpr evaluation.

Error 10: Compiler recursion limits

GCC/Clang limit constexpr recursion depth; use -fconstexpr-depth=N or iterative code.


7. Performance benchmarks

Compile time vs runtime

Precomputing with constexpr gives zero runtime cost for that value. A full example compares factorialRuntime in a loop vs a constexpr precomputed result (typically ~50–200 ms vs ~1–5 ms for 1M iterations—environment-dependent).

CRC32: runtime init vs constexpr table

A full benchmark compares init_crc32_table() at startup against a constexpr CRC32_TABLE; the constexpr table avoids initialization delay at program start.

nextPowerOfTwo and memory

Aspectstd::array (constexpr size)std::vector (runtime size)
AllocationOften stack/staticHeap
LocalityBetterIndirection

8. Production patterns

Pattern 1: Compile-time lookup tables

Parity bits, CRC, encoding tables—build with std::make_index_sequence.

Pattern 2: Parse config from string literals

constexpr int parseSize(const char* s) {
    int result = 0;
    while (*s >= '0' && *s <= '9') {
        result = result * 10 + (*s - '0');
        ++s;
    }
    return result;
}

constexpr int BUF_SIZE = parseSize("4096");
std::array<char, BUF_SIZE> buffer;

Pattern 3: Per-type constants via templates

Use template<typename T> struct TypeTraits with static constexpr size_t max_digits.

Pattern 4: Safe buffer sizes with next power of two

Pattern 5: Protocol buffer size = header + payload

Pattern 6: consteval (C++20)

Forces compile-time-only evaluation—runtime calls are ill-formed.

Pattern 7: Compile-time type id (debugging)

__PRETTY_FUNCTION__ / __FUNCSIG__ in static constexpr strings.

Pattern 8: static_assert with constexpr validators


9. Practical use

Array sizes

nextPowerOfTwo(100) → 128 when n is a constant expression; use for std::array without template metaprogramming for the same effect.

Replacing template metaprogramming

Prefer constexpr int fib(int n) over template<int N> struct Fib for readability; keep Fib<N>::value via std::integral_constant when needed.


  • constexpr guide
  • Compile-time programming #26-2
  • Advanced constexpr

  • constexpr deep dive
  • Compile-time programming
  • Advanced constexpr

Keywords

C++ constexpr, compile-time constant, constexpr function, constexpr vs const, literal type, if constexpr, consteval.


Summary

TopicContent
constexpr variablesCompile-time constants
constexpr functionsCallable in constant expressions when args are constant
constexpr constructorsEnable user-defined literal types
const vs constexprImmutable vs compile-time known
C++14Loops and multi-statement bodies
C++20Broader constexpr, consteval
if constexprCompile-time branches in templates

FAQ

When do I use this in practice?

Whenever sizes, buffers, tables, or validation must be known at build time—use constexpr; use const only for runtime immutability.

Isn’t constexpr slow?

Compile-time evaluation has zero runtime cost when fully folded; variable arguments execute like normal functions.

constexpr vs macros?

Macros are text substitution; constexpr is typed, debuggable, overloadable, and composes with templates.

Does constexpr slow builds?

Heavy constexpr (huge tables, deep recursion) can increase compile time—use judiciously and profile build times.


One-line summary: Use constexpr to compute constants, array sizes, and template arguments at compile time. Next: compile-time programming #26-2.

Next: [C++ Hands-On #26-2] Compile-time programming: templates and constexpr

Previous: [C++ Hands-On #25-3] Custom ranges