Your new mental model for constexpr
-
What is constexpr?
- Moving computation from runtime to compile time
- constexpr is NOT metaprogramming
- Metaprogramming: programs that manipulate other programs as data
- constexpr: regular code, just executed at compile time
-
What can be done at compile time?
- Anything that can be done at runtime can be done at compile time
- C++20 consteval: Turing complete at compile time
- C++11/14/17 constexpr: not Turing complete at compile time
-
The constexpr continuum:
- Deciding how much work you do at compile time vs runtime
- 0% (all runtime) ... constexpr ... 100% (all compile time)
-
Practical use cases for speed:
-
Precomputed lookup tables:
- CRC32, hash functions, trigonometry tables
- Compute once at compile time, look up at runtime
constexpr std::array<uint32_t, 256> make_crc_table() { std::array<uint32_t, 256> table{}; for (uint32_t i = 0; i < 256; i++) { uint32_t crc = i; for (int j = 0; j < 8; j++) { crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0); } table[i] = crc; } return table; } constexpr auto CRC_TABLE = make_crc_table();
-
String processing:
- Convert string encodings at compile time (ASCII to PETSCII, etc.)
- Useful for embedded systems, game dev on retro hardware
constexpr auto PETSCII(const char* str) { // conversion logic } static constexpr auto greeting = PETSCII("Hello World");
-
String tables:
- Store multiple strings in one contiguous memory block
- Better for cache, better for embedded systems with limited RAM
// instead of this (scattered memory) const char* s1 = "Error"; const char* s2 = "Warning"; // do this (one block, built at compile time) constexpr auto LOG_LEVELS = MAKE_STRING_TABLE("Error", "Warning", "Info"); // memory layout: "Error\0Warning\0Info\0" + index array // access: LOG_LEVELS[1] returns "Warning"
-
Complex math at compile time:
- Matrix operations, projection matrices for graphics
- All calculations done by compiler, executable has final values
constexpr Matrix4x4 computeProjectionMatrix(float fov, float aspect) { // trig calculations return {/* ... */}; } constexpr auto PROJ_MATRIX = computeProjectionMatrix(90.0f, 16.0f/9.0f);
-
-
The three keywords:
-
constexpr: may evaluate at compile time
- Compiler can choose to evaluate at compile time or runtime
constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); } constexpr int fact5 = factorial(5); // guaranteed compile time int x = 6; int factX = factorial(x); // runtime (x is not constexpr)
- Compiler can choose to evaluate at compile time or runtime
-
consteval (C++20): must evaluate at compile time
- Forces compile-time evaluation
- Compile error if called with runtime values
consteval int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); } constexpr int x = factorial(5); // OK int n = 5; int y = factorial(n); // compile error
-
constinit (C++20): compile-time initialization, runtime mutability
- For globals/statics that need zero-cost initialization but can change
constinit int counter = 0; // initialized at compile time counter++; // can modify at runtime
- For globals/statics that need zero-cost initialization but can change
-
-
When compiler evaluates at compile time:
- Using in constexpr/consteval context: guaranteed compile time
- Using with static_assert: guaranteed compile time
- Using with const: maybe compile time (compiler decides)
- Using with non-const runtime variables: runtime
constexpr int factorial(int n) { return (n <= 1) ? 1 : n * factorial(n - 1); } constexpr int fact5 = factorial(5); // compile time static_assert(factorial(4) == 24); // compile time const int fact6 = factorial(6); // maybe compile time int x = 7; int factX = factorial(x); // runtime
-
Best practices:
-
When to use:
- Math constants: PI, E, conversion factors
- Lookup tables: CRC, trigonometry, color palettes
- Configuration data that doesn't change
- Powers of 2: use bit shifts computed at compile time
constexpr int MAX_PLAYERS = 64; constexpr float PI = 3.14159265359f; constexpr int KB = 1 << 10; // instead of std::pow(2, 10) constexpr int MB = 1 << 20;
-
Use std::array for constexpr containers:
constexpr std::array<int, 5> nums = {1, 2, 3, 4, 5}; // good constexpr int nums[] = {1, 2, 3, 4, 5}; // old style -
Trade-offs:
- Compilation time increases with complex constexpr
- Binary size grows (precomputed data lives in executable)
- Debugging compile-time code is harder
- Sweet spot: things that truly don't change and are expensive to compute
-
-
Examples of runtime speed-ups:
-
Color parsing in games:
// runtime: parse hex color every frame Color parseHex(const char* hex) { /* parsing */ } draw(parseHex("#FF5733")); // wasteful // compile time: color ready to use constexpr auto FIRE_COLOR = parseHex("#FF5733"); draw(FIRE_COLOR); // just loads the value -
Configuration baking:
// runtime: parse config file at startup Config loadConfig("settings.json"); // compile time: config already in the binary constexpr auto CONFIG = parseConfigFile("settings.json");
-
Key takeaways: - Compile time = prep work, Runtime = serving results - Move expensive work to compile time if value doesn't change - Trade-off: slower compilation for faster execution - constexpr (flexible), consteval (strict), constinit (globals) - Best for: lookup tables, math constants, string processing, config data