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 usegetBufferSize()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
- constexpr variables
- constexpr functions in detail
- constexpr variables and constructors
- constexpr vs const
- C++14/20 extensions
- Common errors
- Performance benchmarks
- Production patterns
- 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
| Aspect | const | constexpr |
|---|---|---|
| Meaning | Not modified after init | Computable at compile time |
| Init | May be runtime | Must be compile time |
| Constant expression | No (if runtime init) | Yes |
| Array bounds | Only if compile-time init | Yes |
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
| Aspect | std::array (constexpr size) | std::vector (runtime size) |
|---|---|---|
| Allocation | Often stack/static | Heap |
| Locality | Better | Indirection |
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.
Good reads (internal links)
- constexpr guide
- Compile-time programming #26-2
- Advanced constexpr
Related posts
- 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
| Topic | Content |
|---|---|
| constexpr variables | Compile-time constants |
| constexpr functions | Callable in constant expressions when args are constant |
| constexpr constructors | Enable user-defined literal types |
| const vs constexpr | Immutable vs compile-time known |
| C++14 | Loops and multi-statement bodies |
| C++20 | Broader constexpr, consteval |
| if constexpr | Compile-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