본문으로 건너뛰기
Previous
Next
C++ constexpr Functions | Compile-Time Functions Explained

C++ constexpr Functions | Compile-Time Functions Explained

C++ constexpr Functions | Compile-Time Functions Explained

이 글의 핵심

C++ constexpr functions: compile-time and runtime use, C++11 vs C++14 vs C++17, arrays, classes, and optimization. Practical examples and pitfalls.

What is a constexpr function?

A constexpr function is a function that may be evaluated at compile time when its arguments are constant expressions, and can still be called at run time like an ordinary function when the arguments are not known until then. That dual nature is what makes constexpr so useful in libraries and embedded code: one implementation, two evaluation contexts.

constexpr int square(int x) {
    return x * x;
}

int main() {
    constexpr int a = square(5);   // compile-time: 25

    int x = 10;
    int b = square(x);               // run time: ordinary call
}

In production, I reach for constexpr when I want guarantees (e.g., “this size is fixed at build time”) or when I want the compiler to fold work away from the hot path—without maintaining a separate macro or code generator.


How constexpr evolved: C++11 through C++20

C++11: the strict beginning

C++11 introduced constexpr with tight rules: the function body was effectively limited to a single return statement (plus some minor forms), and only a narrow set of statement forms was allowed inside constexpr functions. Many algorithms were written recursively because loops were not generally permitted in constexpr functions in the way we use them today.

// C++11 style: single return, recursion for iteration
constexpr int factorial11(int n) {
    return n <= 1 ? 1 : n * factorial11(n - 1);
}

C++14: constexpr becomes practical

C++14 relaxed constexpr functions dramatically: multiple statements, local variables, if/else, and loops became allowed. This is the point where many teams started adopting constexpr for real table generation and small domain logic without contorted recursive style.

// C++14: loops and locals feel "normal"
constexpr int factorial14(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

C++17: if constexpr and more library support

C++17 added if constexpr for templates (not a new constexpr keyword on if in non-template code—see below). The standard library also gained more constexpr algorithms and utilities, making compile-time data structures easier to build. std::array and many <algorithm> pieces work in constexpr contexts when the types allow it.

#include <type_traits>

template <typename T>
constexpr T clamp_typed(T value, T lo, T hi) {
    if constexpr (std::is_integral_v<T>) {
        return value < lo ? lo : (value > hi ? hi : value);
    } else {
        static_assert(std::is_floating_point_v<T>, "only integral or floating types");
        const auto v = static_cast<double>(value);
        const auto a = static_cast<double>(lo);
        const auto b = static_cast<double>(hi);
        if (v < a) return static_cast<T>(a);
        if (v > b) return static_cast<T>(b);
        return value;
    }
}

C++20: consteval, more constexpr in the library, virtual, try/catch

C++20 refined the model further: consteval for mandatory compile-time functions, more constexpr in the standard library (including parts of std::string and std::vector in specified conditions), and expanded rules for constexpr on virtual functions and exceptions in restricted forms. For day-to-day engineering, the headline is: stronger compile-time story, but you must still read the fine print per type (e.g., allocation in constexpr is easy to get wrong).

// C++20: consteval — must run at compile time; ill-formed if not
consteval int must_be_const(int x) {
    return x * x;
}

// const int a = must_be_const(3);     // OK
// int y = 4;
// int b = must_be_const(y);         // error: not a constant expression context

constexpr vs const vs consteval

FeatureMeaningTypical use
constImmutability after initialization; initialization may be run timeFunction-local constants, “do not reassigned”
constexpr (variable)True compile-time constantArray bounds, template non-type parameters, static data in headers
constexpr (function)May run at compile time if inputs are constant expressionsShared helpers for compile-time and run time
constevalMust run at compile timeDomain-specific “mini languages” evaluated only at build time
int get_value();  // not constexpr

const int x = get_value();          // OK: const, possibly run-time init
// constexpr int y = get_value();   // error: not a constant expression

constexpr int z = 42;               // OK: compile-time constant

consteval int triple(int n) { return 3 * n; }
constexpr int t1 = triple(5);       // OK

Practical rule from the field: use constexpr on functions whenever the computation is pure and cheap enough for the compiler— it documents intent and keeps the door open for compile-time evaluation. Use consteval when run-time evaluation would be a logic bug (e.g., parsing a DSL that must not exist in the binary path).


Compile-time calculation: patterns that pay off

Lookup tables and polynomial evaluation

Precomputing coefficients or small tables at compile time removes branches and memory traffic from hot loops.

constexpr double eval_poly(double x) {
    // Horner form: 1 + 2x + 3x^2  (illustrative)
    return 1.0 + x * (2.0 + x * 3.0);
}

constexpr double c0 = eval_poly(0.5);

String hashing for switch-like dispatch

In games and network code, I have used constexpr string hashes for event names. The full string may exist only as a compile-time literal; the integer is what you compare at run time.

constexpr unsigned int djb2(const char* str) {
    unsigned int h = 5381;
    while (*str) {
        h = ((h << 5) + h) + static_cast<unsigned char>(*str);
        ++str;
    }
    return h;
}

enum class Event : unsigned int {
    Connect = djb2("connect"),
    Ping    = djb2("ping"),
};

Bit masks and protocol constants

Struct layouts and flag sets that must never drift from the spec are excellent constexpr candidates.

constexpr std::uint32_t make_mask(int low_bit, int width) {
    return ((1u << width) - 1u) << low_bit;
}

constexpr std::uint32_t kFrameFlags = make_mask(0, 4);

Constraints and tips for writing constexpr functions

  1. Only constexpr-friendly operations inside: no I/O, no new (except under C++20 rules with careful use), no undefined behavior you rely on.
  2. Keep functions small and pure—easier for humans and for the compiler’s constant evaluator.
  3. Test both paths: constexpr test with static_assert, and a run-time unit test with dynamic inputs.
  4. Watch evaluation limits: compilers may cap recursion depth or step count; huge constexpr computations can fail at compile time with a hard error.
constexpr int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);  // fine for small n; explodes for large n
}

static_assert(fib(10) == 55);

Avoiding hidden non-constexpr dependencies

int non_constexpr_helper(int x);

constexpr int bad(int x) {
    return non_constexpr_helper(x);  // error: not usable in constexpr evaluation
}

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

if constexpr and template metaprogramming

if constexpr is a compile-time branch for templates: the branch that is not taken is discarded and not instantiated, which avoids invalid expressions for other types.

#include <type_traits>

template <typename T>
constexpr T abs_sum(T a, T b) {
    if constexpr (std::is_unsigned_v<T>) {
        return a + b;
    } else {
        const T aa = a < 0 ? -a : a;
        const T bb = b < 0 ? -b : b;
        return aa + bb;
    }
}

In real code, add a static_assert in an else branch when the set of types is closed. if constexpr shines for SFINAE-free dispatch and for reading code that used to be an unreadable tangle of enable_if.


std::array and compile-time initialization

std::array is often the right vessel for fixed-size data produced at compile time: it carries size in the type, works with range-for, and plays well with templates.

#include <array>

constexpr int square(int i) { return i * i; }

template <std::size_t N>
constexpr std::array<int, N> make_squares() {
    std::array<int, N> a{};
    for (std::size_t i = 0; i < N; ++i) {
        a[i] = static_cast<int>(square(static_cast<int>(i)));
    }
    return a;
}

inline constexpr auto kSquares10 = make_squares<10>();

static_assert(kSquares10[3] == 9);

String helpers (constexpr-friendly)

constexpr std::size_t strlen_ce(const char* s) {
    std::size_t n = 0;
    while (s[n] != '\0') {
        ++n;
    }
    return n;
}

constexpr bool starts_with(const char* s, const char* prefix) {
    while (*prefix) {
        if (*s != *prefix) return false;
        ++s;
        ++prefix;
    }
    return true;
}

Performance measurement and benchmarks

What to expect: successful compile-time evaluation removes work from run time if the result is actually used as a constant or folded into instructions. If you still load from a static readonly variable, you may see memory traffic instead of immediate operands—profile before celebrating.

A minimal Google Benchmark–style sketch:

#include <benchmark/benchmark.h>
#include <array>

constexpr std::array<int, 1024> make_table() {
    std::array<int, 1024> t{};
    for (int i = 0; i < 1024; ++i) t[static_cast<std::size_t>(i)] = i * i;
    return t;
}

static constexpr auto kTable = make_table();

static void BM_use_table(benchmark::State& state) {
    int acc = 0;
    for (auto _ : state) {
        benchmark::DoNotOptimize(acc += kTable[static_cast<std::size_t>(acc & 1023)]);
    }
}
BENCHMARK(BM_use_table);

Practical tip: compare assembly (-S / Compiler Explorer) for constexpr versus run-time initialization; also check binary size when large tables move to read-only data.


Real-world use cases

  1. Embedded firmware: fixed-size ring buffers, CRC tables, and unit conversion factors guaranteed at build time.
  2. Games:constexpr hashes, bit-packed schema definitions, and validation of asset metadata sizes.
  3. High-frequency trading and low-latency services: precomputing decision thresholds and masks; keeping critical paths branch-light.
  4. Header-only libraries: constexpr API that works in static_assert tests users write in their own code.
class Point2D {
    int x_{};
    int y_{};

public:
    constexpr Point2D(int x, int y) : x_{x}, y_{y} {}
    constexpr int x() const { return x_; }
    constexpr int y() const { return y_; }
    constexpr int manhattan() const { return (x_ < 0 ? -x_ : x_) + (y_ < 0 ? -y_ : y_); }
};

static_assert(Point2D{3, 4}.manhattan() == 7);

Pitfalls, limitations, and debugging

Calling non-constexpr functions

int runtime_only(int v);

constexpr int oops(int v) {
    return runtime_only(v);  // ill-formed in constexpr evaluation
}

Run-time-only inputs

constexpr int f(int x) { return x * x; }

void read_user(int& v) { /* ... */ }

void example() {
    int x = 0;
    read_user(x);
    int y = f(x);  // run-time call — fine
    // constexpr int z = f(x);  // error
}

virtual and constexpr (C++20)

C++20 allows constexpr virtual functions under specific conditions; do not assume the whole hierarchy is constexpr-friendly. Verify on your target compiler and standard mode.

Debugging compile-time failures

When a constexpr function fails during constant evaluation, compilers emit errors pointing inside the function. Binary search with static_assert on subexpressions, and reduce templates until the minimal repro is obvious.

constexpr int div_safe(int a, int b) {
    return b == 0 ? a : a / b;  // still avoid 0/0 UB in real code; illustrate guarding
}

Over-eager constexpr cost

Compile times can grow if you build enormous graphs at compile time. If a table generation template instantiates thousands of times, you may be paying build performance for small run savings—measure both.


constexpr classes (quick recap)

class Point {
    int x, y;
public:
    constexpr Point(int x, int y) : x(x), y(y) {}
    constexpr int getX() const { return x; }
    constexpr int getY() const { return y; }
    constexpr int distanceSquared() const { return x * x + y * y; }
};

int main() {
    constexpr Point p(3, 4);
    constexpr int dist = p.distanceSquared();
    int arr[dist];  // 25: compile-time size (where supported)
    (void)arr;
}

constexpr vs inline

inline is about ODR and inlining hints across translation units. constexpr is about constant evaluation eligibility. A function can be inline constexpr, and often is in headers.


Takeaways

  • constexpr functions are the workhorse of modern compile-time C++: one definition serves constant evaluation and run time.
  • Evolution from C++11 to C++20 made the feature ergonomic (consteval, more library support, more flexible rules).
  • Use if constexpr to simplify template metaprogramming; use consteval when run-time use would be meaningless.
  • Validate with static_assert, watch compiler limits, and profile build time and binary size for large constexpr data.

FAQ

Q: When to use constexpr? For compile-time constants, array sizes, template metaprogramming, and to document pure functions that should be evaluable at build time when possible.

Q: Can constexpr functions run at run time? Yes, when the arguments are not constant expressions, evaluation happens like a normal function.

Q: Learning resources: Effective Modern C++ (Meyers), cppreference constexpr, and C++ Templates: The Complete Guide (Vandevoorde et al.).

Keywords

C++, constexpr, compile-time, C++11

Note on source

The repository had no cpp-constexpr.md; this English article is based on cpp-constexpr-function.md and saved as cpp-constexpr-en.md per your naming request.

See also (internal)