A constexpr function in C++ is a function that can be evaluated at compile time when called with constant expressions, producing results embedded directly into the compiled program. When called with non-constant arguments, it behaves as a regular runtime function. Declared with the constexpr keyword, these functions must have a body that the compiler can evaluate: no runtime I/O, no dynamic allocation (before C++20), no undefined behavior. C++20 dramatically expanded what is allowed in constexpr context — including std::vector, std::string, try/catch, and virtual functions.
Introduction
Every program has two phases: compile time and runtime. Traditionally, all computation happens at runtime — the CPU executes instructions when the program runs. But much of what programs compute is actually known at compile time: mathematical constants, lookup tables, string hashes, configuration structures, CRC tables, prime sieves. Computing these at runtime wastes CPU cycles on work that could have been done once, by the compiler, with the result baked into the binary.
constexpr is C++’s mechanism for moving computation from runtime to compile time. A constexpr function or variable is evaluated by the compiler itself — not the CPU — and its result is embedded in the compiled code as a literal. From the program’s perspective, reading a constexpr computed value is like reading the constant 42: there is no instruction to execute, just a value already present.
The benefits go beyond raw performance. Compile-time computation enables things that are impossible or unsafe at runtime: constexpr values can be used as template arguments, array sizes, and switch case labels. Compile-time errors are better than runtime errors — if a constexpr function contains undefined behavior or violates a precondition, the compiler reports an error rather than the program crashing in production.
C++11 introduced constexpr with severe restrictions — only a single return statement allowed. C++14 relaxed this dramatically, allowing loops, local variables, and most control flow. C++17 made if constexpr work with constexpr functions. C++20 completed the transformation, allowing dynamic allocation, virtual functions, try/catch, std::vector, and std::string in constexpr context — making nearly any computation expressible at compile time.
This article teaches constexpr from first principles through C++20’s expanded capabilities, with practical examples showing when and how to move computation to compile time.
The Basics: constexpr Variables and Functions
#include <iostream>
using namespace std;
// constexpr variable: evaluated at compile time
constexpr double PI = 3.14159265358979;
constexpr int MAX_BUF = 1024;
constexpr bool DEBUG = false;
// constexpr function: can be evaluated at compile time
constexpr int square(int x) {
return x * x;
}
constexpr int factorial(int n) {
// C++14+: loops and local variables are allowed
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
constexpr double circleArea(double radius) {
return PI * radius * radius;
}
int main() {
// Compile-time evaluation: result is a compile-time constant
constexpr int s = square(7); // Computed by compiler: s = 49
constexpr int f = factorial(10); // Computed by compiler: f = 3628800
constexpr double area = circleArea(5.0); // Computed by compiler
cout << "7 squared: " << s << endl;
cout << "10 factorial: " << f << endl;
cout << "Area of r=5: " << area << endl;
// constexpr values as template arguments (requires compile-time constant)
array<int, square(8)> arr; // array size = 64, computed at compile time
cout << "Array size: " << arr.size() << endl;
// constexpr values as switch case labels
constexpr int CODE = 42;
switch (CODE) {
case square(6): cout << "CODE is 36" << endl; break;
case factorial(3): cout << "CODE is 6" << endl; break;
case 42: cout << "CODE is 42" << endl; break;
}
// Runtime evaluation: same function, runtime argument
int runtimeValue;
cin >> runtimeValue; // Read at runtime
int runtimeSquare = square(runtimeValue); // Evaluated at runtime
cout << runtimeValue << " squared = " << runtimeSquare << endl;
// Demonstrating compile-time vs runtime distinction
constexpr int compile_result = square(10); // Must be compile-time
int runtime_n = 10;
int runtime_result = square(runtime_n); // Runtime — n is not constexpr
cout << "compile_result = " << compile_result << endl;
cout << "runtime_result = " << runtime_result << endl;
return 0;
}
Sample output (with input 5):
7 squared: 49
10 factorial: 3628800
Area of r=5: 78.5398
Array size: 64
CODE is 42
5 squared = 25
compile_result = 100
runtime_result = 100
Step-by-step explanation:
constexpr int s = square(7)forces compile-time evaluation because the result is stored in aconstexprvariable. The compiler must be able to evaluatesquare(7)during compilation — if it cannot (e.g., the function reads from stdin), it produces a compile error.array<int, square(8)>uses the result as a template non-type argument. Template arguments must be compile-time constants — this is only possible becausesquare(8)is a constant expression. With a non-constexprfunction, this would be a compile error.int runtimeSquare = square(runtimeValue)evaluatessquareat runtime becauseruntimeValueis not a compile-time constant (it was read from stdin). The sameconstexprfunction executes both paths — compile-time when given constant arguments, runtime otherwise.constexprvariables (PI,MAX_BUF) are like#defineconstants but type-safe, scoped, and debuggable. Unlike#define, they participate in the type system and cannot cause macro-expansion surprises.- The distinction:
constexpr int x = f()requires compile-time evaluation.int x = f()allows but does not require compile-time evaluation — the compiler may or may not optimize it. Onlyconstexprguarantees compile-time execution.
C++14 Relaxations: Loops, Branches, and Local Variables
C++11 constexpr functions were restricted to a single return statement — effectively only recursive expressions were possible. C++14 removed most restrictions, enabling natural imperative-style code.
#include <iostream>
#include <array>
using namespace std;
// Binary search at compile time (C++14 style)
constexpr int binarySearch(const int* arr, int size, int target) {
int lo = 0, hi = size - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] == target) return mid;
else if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1; // Not found
}
// Compile-time string length
constexpr size_t strLen(const char* s) {
size_t len = 0;
while (s[len] != '\0') ++len;
return len;
}
// Compile-time power function with integer exponent
constexpr long long power(long long base, int exp) {
if (exp < 0) return 0; // Error case
if (exp == 0) return 1;
long long result = 1;
long long b = base;
int e = exp;
while (e > 0) {
if (e & 1) result *= b; // Exponentiation by squaring
b *= b;
e >>= 1;
}
return result;
}
// Compile-time GCD (Euclidean algorithm)
constexpr int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
// Compile-time prime check
constexpr bool isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
// Compile-time FNV-1a hash for string literals
constexpr uint32_t fnv1a(const char* s) {
uint32_t hash = 2166136261u;
while (*s) {
hash ^= static_cast<uint8_t>(*s++);
hash *= 16777619u;
}
return hash;
}
int main() {
// Binary search: both array and search are compile-time
constexpr int sortedData[] = {2, 5, 8, 12, 16, 23, 38, 56, 72, 91};
constexpr int idx = binarySearch(sortedData, 10, 23);
cout << "Index of 23: " << idx << endl; // 5
constexpr int notFound = binarySearch(sortedData, 10, 99);
cout << "Index of 99: " << notFound << endl; // -1
// String operations at compile time
constexpr size_t len = strLen("Hello, World!");
cout << "String length: " << len << endl; // 13
static_assert(len == 13, "Length should be 13");
// Power computations
constexpr long long two20 = power(2, 20); // 1048576
constexpr long long ten6 = power(10, 6); // 1000000
cout << "2^20 = " << two20 << endl;
cout << "10^6 = " << ten6 << endl;
// GCD
constexpr int g = gcd(48, 18); // 6
cout << "gcd(48, 18) = " << g << endl;
// Prime detection as template arguments
constexpr bool p17 = isPrime(17); // true
constexpr bool p18 = isPrime(18); // false
cout << "17 is prime: " << p17 << endl;
cout << "18 is prime: " << p18 << endl;
// String hashing at compile time — usable in switch statements
constexpr uint32_t HASH_START = fnv1a("start");
constexpr uint32_t HASH_STOP = fnv1a("stop");
constexpr uint32_t HASH_RESET = fnv1a("reset");
string command = "stop"; // Runtime string
uint32_t commandHash = fnv1a(command.c_str()); // Runtime hash
switch (commandHash) {
case HASH_START: cout << "Executing: start" << endl; break;
case HASH_STOP: cout << "Executing: stop" << endl; break;
case HASH_RESET: cout << "Executing: reset" << endl; break;
default: cout << "Unknown command" << endl; break;
}
return 0;
}
Output:
Index of 23: 5
Index of 99: -1
String length: 13
2^20 = 1048576
10^6 = 1000000
gcd(48, 18) = 6
17 is prime: 1
18 is prime: 0
Executing: stop
Step-by-step explanation:
binarySearchuses awhileloop — impossible in C++11constexpr, completely natural in C++14+. The array and size are compile-time constants (passed asconstexprvalues), so the entire search happens at compile time.strLen("Hello, World!")iterates a null-terminated string at compile time. String literals are compile-time constants — their characters are known to the compiler. The result13is embedded in the binary as a literal.power(2, 20)uses exponentiation by squaring — a standard algorithm, implemented with awhileloop and bit operations. The compiler executes this algorithm during compilation and embeds1048576in the binary.- The FNV-1a hash trick:
constexprhash values of string literals can be used asswitchcase labels. At runtime, you hash the input string and compare against the precomputed hashes. This converts string dispatch (if (cmd == "start")) into integer comparison, which is faster for many commands. static_assert(len == 13)verifies compile-time results — a compile-time assertion that fires if the condition is false. This is a zero-runtime-cost correctness check: it cannot possibly slow down the program because it does not exist at runtime.
Compile-Time Lookup Tables
One of the most practical uses of constexpr is generating lookup tables — large precomputed arrays that would be expensive to compute at runtime.
#include <iostream>
#include <array>
using namespace std;
// Generate a complete CRC8 lookup table at compile time
constexpr uint8_t crc8_byte(uint8_t byte) {
for (int i = 0; i < 8; i++) {
if (byte & 0x80)
byte = (byte << 1) ^ 0x07; // CRC-8/SMBUS polynomial
else
byte <<= 1;
}
return byte;
}
constexpr array<uint8_t, 256> makeCRC8Table() {
array<uint8_t, 256> table{};
for (int i = 0; i < 256; i++) {
table[i] = crc8_byte(static_cast<uint8_t>(i));
}
return table;
}
constexpr auto CRC8_TABLE = makeCRC8Table();
// Compute CRC8 using the precomputed table
uint8_t crc8(const uint8_t* data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; i++) {
crc = CRC8_TABLE[crc ^ data[i]];
}
return crc;
}
// Sine approximation table (fixed-point, 256 entries for 0-360 degrees)
constexpr array<int16_t, 256> makeSineTable() {
array<int16_t, 256> table{};
// We use the Taylor series for sin since <cmath> may not be constexpr
// sin(x) ≈ x - x³/6 + x⁵/120 - x⁷/5040
constexpr double PI2 = 6.28318530718;
for (int i = 0; i < 256; i++) {
double angle = (i * PI2) / 256.0;
// Taylor series (5 terms for reasonable accuracy)
double x = angle;
double s = x;
double term = x;
for (int n = 1; n <= 5; n++) {
term *= -(x * x) / ((2*n) * (2*n + 1));
s += term;
}
// Scale to int16_t range: -32767 to 32767
table[i] = static_cast<int16_t>(s * 32767.0);
}
return table;
}
constexpr auto SINE_TABLE = makeSineTable();
// Perfect powers table: is N a perfect square, cube, etc.?
constexpr array<bool, 100> makePerfectSquares() {
array<bool, 100> table{};
for (int i = 0; i < 10; i++) {
table[i * i] = true;
}
return table;
}
constexpr auto PERFECT_SQUARES = makePerfectSquares();
// Fibonacci lookup table
constexpr array<uint64_t, 93> makeFibTable() {
array<uint64_t, 93> table{};
table[0] = 0; table[1] = 1;
for (int i = 2; i < 93; i++) {
table[i] = table[i-1] + table[i-2];
}
return table;
}
constexpr auto FIB_TABLE = makeFibTable();
int main() {
cout << "=== CRC8 table (first 8 entries) ===" << endl;
for (int i = 0; i < 8; i++) {
cout << " CRC8_TABLE[" << i << "] = "
<< static_cast<int>(CRC8_TABLE[i]) << endl;
}
// Use the CRC table to verify data integrity
const uint8_t data[] = {0x01, 0x02, 0x03, 0x04, 0x05};
uint8_t checksum = crc8(data, sizeof(data));
cout << "CRC8 of {1,2,3,4,5} = 0x"
<< hex << static_cast<int>(checksum) << dec << endl;
cout << "\n=== Sine table samples ===" << endl;
// Index 0 = 0°, 64 = 90°, 128 = 180°, 192 = 270°
cout << "sin(0°) = " << SINE_TABLE[0] / 32767.0 << endl; // ≈ 0
cout << "sin(90°) = " << SINE_TABLE[64] / 32767.0 << endl; // ≈ 1
cout << "sin(180°) = " << SINE_TABLE[128] / 32767.0 << endl; // ≈ 0
cout << "sin(270°) = " << SINE_TABLE[192] / 32767.0 << endl; // ≈ -1
cout << "\n=== Perfect squares under 100 ===" << endl;
cout << "Perfect squares: ";
for (int i = 0; i < 100; i++) {
if (PERFECT_SQUARES[i]) cout << i << " ";
}
cout << endl;
cout << "\n=== Fibonacci numbers ===" << endl;
cout << "First 10 Fibonacci: ";
for (int i = 0; i < 10; i++) cout << FIB_TABLE[i] << " ";
cout << endl;
cout << "Fib(50) = " << FIB_TABLE[50] << endl;
cout << "Fib(92) = " << FIB_TABLE[92] << endl;
// These tables exist entirely in the .rodata section —
// zero runtime computation needed
static_assert(FIB_TABLE[10] == 55, "Fibonacci check");
static_assert(PERFECT_SQUARES[49], "49 is a perfect square");
return 0;
}
Output:
=== CRC8 table (first 8 entries) ===
CRC8_TABLE[0] = 0
CRC8_TABLE[1] = 7
CRC8_TABLE[2] = 14
CRC8_TABLE[3] = 9
CRC8_TABLE[4] = 28
CRC8_TABLE[5] = 27
CRC8_TABLE[6] = 18
CRC8_TABLE[7] = 21
CRC8 of {1,2,3,4,5} = 0x77
=== Sine table samples ===
sin(0°) = 0
sin(90°) = 1
sin(180°) = -0.000479
sin(270°) = -1
=== Perfect squares under 100 ===
Perfect squares: 0 1 4 9 16 25 36 49 64 81
=== Fibonacci numbers ===
First 10 Fibonacci: 0 1 1 2 3 5 8 13 21 34
Fib(50) = 12586269025
Fib(92) = 7540113804746346429
Step-by-step explanation:
constexpr auto CRC8_TABLE = makeCRC8Table()generates the entire 256-entry CRC8 lookup table at compile time. The resulting array is placed in the binary’s read-only data section (.rodata). At runtime,crc8()simply indexes into this precomputed table — no computation needed.makeSineTable()returns aconstexpr array<int16_t, 256>computed using a Taylor series. This avoids the need for<cmath>‘ssin()(which may not beconstexprin all implementations) while providing a reasonably accurate fixed-point sine table for embedded or real-time use.makeFibTable()memoizes all Fibonacci numbers up to the maximum that fits inuint64_t(the 93rd). Without this table, computingFIB_TABLE[92]at runtime would require iteration; with it, it’s a single array access — O(1) vs O(n).static_assert(FIB_TABLE[10] == 55)verifies the table is correct at compile time. This is a zero-cost regression test embedded in the code — the compiler checks it every build.- All four tables compile to static data in the binary. The program starts with all values precomputed — no initialization loop, no first-use overhead. This is particularly valuable for embedded systems where startup time matters.
consteval and constinit (C++20)
C++20 introduced two new keywords that give finer control over compile-time evaluation:
#include <iostream>
#include <stdexcept>
using namespace std;
// consteval: MUST be evaluated at compile time — always
// If called with non-constant arguments, it's a compile ERROR
consteval int mustBeCompileTime(int x) {
return x * x + 1;
}
// constexpr: CAN be compile-time, may be runtime
constexpr int canBeEither(int x) {
return x * x + 1;
}
// consteval is useful for:
// 1. Template-argument-like values from function syntax
consteval int makeFlag(int bit) {
if (bit < 0 || bit > 31) throw out_of_range("bit out of range");
return 1 << bit;
}
// 2. Compile-time ID generation
consteval size_t typeId(const char* name) {
size_t hash = 14695981039346656037ull;
while (*name) {
hash ^= static_cast<uint8_t>(*name++);
hash *= 1099511628211ull;
}
return hash;
}
// constinit: variable is guaranteed to be zero-initialized at compile time
// (not a constant, but guaranteed constant initialization — no static init order fiasco)
constinit int globalCounter = 0; // OK: constant initialization
constinit double gravity = 9.81; // OK: constant initialization
// constinit int bad = rand(); // ERROR: rand() not constexpr
struct Config {
constinit static int maxConnections; // Declared constinit — no fiasco
};
constinit int Config::maxConnections = 100;
// Combining consteval with if consteval (C++23 preview concept)
// For C++20: use a helper template
constexpr int sqrt_approx(int n) {
// This works at both compile time and runtime
int result = 0;
while ((result + 1) * (result + 1) <= n) result++;
return result;
}
int main() {
cout << "=== consteval ===" << endl;
// OK: compile-time constant argument
constexpr int a = mustBeCompileTime(7); // Evaluated at compile time
cout << "mustBeCompileTime(7) = " << a << endl;
// OK: used directly in constexpr context
static_assert(mustBeCompileTime(5) == 26, "5*5+1=26");
// COMPILE ERROR (commented out — would not compile):
// int x = 5;
// int b = mustBeCompileTime(x); // ERROR: x is not constexpr
// canBeEither works at both:
constexpr int c = canBeEither(7); // Compile time
int y = 7;
int d = canBeEither(y); // Runtime
cout << "canBeEither(7) compile: " << c << endl;
cout << "canBeEither(7) runtime: " << d << endl;
cout << "\n=== makeFlag (bit flags) ===" << endl;
constexpr int FLAG_READ = makeFlag(0); // 1
constexpr int FLAG_WRITE = makeFlag(1); // 2
constexpr int FLAG_EXECUTE = makeFlag(2); // 4
cout << "READ: " << FLAG_READ << endl;
cout << "WRITE: " << FLAG_WRITE << endl;
cout << "EXECUTE: " << FLAG_EXECUTE << endl;
// COMPILE ERROR: bit out of range:
// constexpr int badFlag = makeFlag(40); // throws at compile time → error
cout << "\n=== typeId for compile-time type tags ===" << endl;
constexpr size_t intId = typeId("int");
constexpr size_t stringId = typeId("std::string");
constexpr size_t vecId = typeId("std::vector<int>");
cout << "typeId(\"int\") = " << intId << endl;
cout << "typeId(\"std::string\") = " << stringId << endl;
cout << "typeId(\"std::vector<int>\")= " << vecId << endl;
cout << "All unique: "
<< (intId != stringId && stringId != vecId ? "yes" : "no") << endl;
cout << "\n=== constinit globals ===" << endl;
cout << "globalCounter: " << globalCounter << endl;
cout << "gravity: " << gravity << endl;
cout << "maxConnections:" << Config::maxConnections << endl;
globalCounter++; // constinit allows runtime modification
cout << "After ++: " << globalCounter << endl;
cout << "\n=== sqrt_approx (constexpr) ===" << endl;
constexpr int sq16 = sqrt_approx(16); // Compile time: 4
constexpr int sq2 = sqrt_approx(2); // Compile time: 1
int n = 100;
int sqN = sqrt_approx(n); // Runtime
cout << "sqrt_approx(16) = " << sq16 << endl;
cout << "sqrt_approx(2) = " << sq2 << endl;
cout << "sqrt_approx(100)= " << sqN << endl;
return 0;
}
Output:
=== consteval ===
mustBeCompileTime(7) = 50
canBeEither(7) compile: 50
canBeEither(7) runtime: 50
=== makeFlag (bit flags) ===
READ: 1
WRITE: 2
EXECUTE: 4
=== typeId for compile-time type tags ===
typeId("int") = 6902316333161909273
typeId("std::string") = 16428624622017578882
typeId("std::vector<int>")= 9847890625327034792
All unique: yes
=== constinit globals ===
globalCounter: 0
gravity: 9.81
maxConnections:100
After ++: 1
=== sqrt_approx (constexpr) ===
sqrt_approx(16) = 4
sqrt_approx(2) = 1
sqrt_approx(100)= 10
Step-by-step explanation:
constevalmakes a function “immediate” — it is always executed at compile time. If you callmustBeCompileTime(x)wherexis a runtime variable, the compiler produces an error. Useconstevalto enforce that a function is always used in a compile-time context.makeFlag(40)throwsout_of_range— a runtime exception — but sinceconstevalforces compile-time execution, the throw becomes a compile-time error with a clear diagnostic. This is a clean way to validate inputs at compile time using familiar exception syntax.typeIdis a compile-time FNV-1a hash of a string, producing unique IDs for type names. Combined withconsteval, it becomes a compile-time type tagging system: each call with a unique string produces a uniquesize_t, usable as a compile-time discriminator.constinitsolves the static initialization order fiasco for global variables. Aconstinitglobal is guaranteed to be initialized before any dynamic initialization runs — beforemain(), before any other global constructors. Unlikeconstexpr, aconstinitvariable can be modified at runtime. It just guarantees its initial value is set at compile time.- The key difference summary:
constexpr= can be compile-time or runtime;consteval= must be compile-time;constinit= initial value set at compile time, modifiable at runtime.
C++20: Dynamic Allocation in constexpr
C++20 dramatically expanded what is allowed in constexpr context — including dynamic allocation with new/delete and standard library containers like std::vector and std::string.
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
using namespace std;
// C++20: vector operations in constexpr
constexpr int vectorSum(initializer_list<int> values) {
vector<int> v(values); // Dynamic allocation OK in C++20 constexpr!
return accumulate(v.begin(), v.end(), 0);
}
constexpr vector<int> sieve(int limit) {
vector<bool> isComposite(limit + 1, false);
vector<int> primes;
for (int i = 2; i <= limit; i++) {
if (!isComposite[i]) {
primes.push_back(i);
for (int j = i * 2; j <= limit; j += i) {
isComposite[j] = true;
}
}
}
return primes;
}
constexpr string joinStrings(const vector<string>& parts, const string& sep) {
string result;
for (size_t i = 0; i < parts.size(); i++) {
if (i > 0) result += sep;
result += parts[i];
}
return result;
}
// C++20: try/catch in constexpr (exception must not propagate)
constexpr int safeDivide(int a, int b) {
try {
if (b == 0) throw runtime_error("division by zero");
return a / b;
} catch (const runtime_error&) {
return 0; // Default value on error
}
}
// C++20: virtual functions in constexpr
struct Shape {
virtual constexpr double area() const = 0;
virtual ~Shape() = default;
};
struct Circle : Shape {
double r;
constexpr Circle(double radius) : r(radius) {}
constexpr double area() const override {
return 3.14159 * r * r;
}
};
struct Square : Shape {
double side;
constexpr Square(double s) : side(s) {}
constexpr double area() const override {
return side * side;
}
};
constexpr double totalArea() {
Circle c(5.0);
Square s(4.0);
// Virtual dispatch in constexpr context (C++20)
Shape* shapes[] = {&c, &s};
double total = 0;
for (auto* shape : shapes) total += shape->area();
return total;
}
int main() {
cout << "=== C++20 constexpr with vector ===" << endl;
constexpr int sum = vectorSum({1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
cout << "Sum 1..10 = " << sum << endl;
static_assert(sum == 55);
// Sieve of Eratosthenes at compile time
// (Note: constexpr vector can't cross translation unit boundary as a constant,
// but works great as a local constexpr or within constexpr functions)
constexpr int LIMIT = 50;
// For cross-boundary use, materialize to array:
constexpr auto primesUnder50 = []() {
auto primes = sieve(LIMIT);
array<int, 15> result{}; // 15 primes under 50
for (size_t i = 0; i < result.size() && i < primes.size(); i++)
result[i] = primes[i];
return result;
}();
cout << "Primes under 50: ";
for (int p : primesUnder50) if (p) cout << p << " ";
cout << endl;
cout << "\n=== String operations in constexpr ===" << endl;
// constexpr strings work within a single constexpr evaluation
constexpr int dotCount = []() {
string s = "www.example.com";
int count = 0;
for (char c : s) if (c == '.') count++;
return count;
}();
cout << "Dots in 'www.example.com': " << dotCount << endl;
static_assert(dotCount == 2);
cout << "\n=== safeDivide with try/catch ===" << endl;
constexpr int d1 = safeDivide(10, 3); // 3
constexpr int d2 = safeDivide(10, 0); // 0 (caught)
cout << "10/3 = " << d1 << endl;
cout << "10/0 = " << d2 << " (division by zero handled)" << endl;
cout << "\n=== Virtual functions in constexpr ===" << endl;
constexpr double total = totalArea();
cout << "Total area (circle r=5 + square s=4) = " << total << endl;
// 3.14159*25 + 16 = 78.539 + 16 = 94.539
static_assert(totalArea() > 94.0 && totalArea() < 95.0);
cout << "Verified at compile time!" << endl;
return 0;
}
Output:
=== C++20 constexpr with vector ===
Sum 1..10 = 55
Primes under 50: 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
=== String operations in constexpr ===
Dots in 'www.example.com': 2
=== safeDivide with try/catch ===
10/3 = 3
10/0 = 0 (division by zero handled)
=== Virtual functions in constexpr ===
Total area (circle r=5 + square s=4) = 94.5398
Verified at compile time!
Step-by-step explanation:
vector<int> v(values)inside aconstexprfunction works in C++20 becausenew/deleteare permitted inconstexprcontext — with the constraint that all memory allocated during compile-time evaluation must be deallocated within the same evaluation. No compile-time memory leaks are permitted.- The sieve of Eratosthenes uses
vector<bool>andvector<int>at compile time. The primes are computed by the compiler during compilation — the binary contains the result, not the algorithm execution. try/catchinconstexpr(C++20) allows error handling during compile-time evaluation. If an exception would propagate out of theconstexprevaluation (uncaught), it becomes a compile error. If caught internally, evaluation continues. This lets you writeconstexprfunctions that validate inputs gracefully.- Virtual functions in
constexprcontext (C++20) work because the compiler can resolve the virtual dispatch statically when the dynamic type is known at compile time.shape->area()for aCircle*resolves toCircle::area()at compile time. - The lambda
[]() { ... }()pattern is the idiomatic way to execute complexconstexprcode blocks and assign their result to aconstexprvariable — an immediately invoked lambda used as a compile-time scope.
Performance: Measuring the Benefit
#include <iostream>
#include <chrono>
using namespace std;
// Expensive computation: count prime numbers up to N
// Runtime version
int countPrimesRuntime(int limit) {
int count = 0;
for (int n = 2; n <= limit; n++) {
bool prime = true;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) { prime = false; break; }
}
if (prime) count++;
}
return count;
}
// Compile-time version
constexpr int countPrimesCompiletime(int limit) {
int count = 0;
for (int n = 2; n <= limit; n++) {
bool prime = true;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) { prime = false; break; }
}
if (prime) count++;
}
return count;
}
// The result: baked into binary, read as a constant
constexpr int PRIMES_UNDER_10000 = countPrimesCompiletime(10000);
int main() {
// Runtime measurement
auto t0 = chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; i++) {
volatile int result = countPrimesRuntime(10000);
(void)result;
}
auto t1 = chrono::high_resolution_clock::now();
double runtimeMs = chrono::duration<double,milli>(t1-t0).count();
// "Compile-time" measurement — just reading a constant
auto t2 = chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; i++) {
volatile int result = PRIMES_UNDER_10000; // Just a load instruction
(void)result;
}
auto t3 = chrono::high_resolution_clock::now();
double constexprMs = chrono::duration<double,milli>(t3-t2).count();
cout << "Primes under 10000: " << PRIMES_UNDER_10000 << endl;
cout << "\nRuntime (1000 calls): " << runtimeMs << " ms" << endl;
cout << "constexpr (1000 reads):" << constexprMs << " ms" << endl;
cout << "Speedup: " << runtimeMs / max(constexprMs, 0.001) << "x" << endl;
// Verify both give the same answer
static_assert(PRIMES_UNDER_10000 == 1229, "1229 primes under 10000");
cout << "\nVerified: 1229 primes under 10000 (compile-time assertion passed)" << endl;
return 0;
}
Output:
Primes under 10000: 1229
Runtime (1000 calls): 847 ms
constexpr (1000 reads): 0.002 ms
Speedup: 423500x
Verified: 1229 primes under 10000 (compile-time assertion passed)
The 1229 primes under 10,000 are computed once by the compiler. The 1000 “constexpr reads” at runtime are simply loading the constant 1229 from memory — effectively free.
constexpr Summary: Evolution Across Standards
| Feature | C++11 | C++14 | C++17 | C++20 |
|---|---|---|---|---|
| Single return statement | ✓ | ✓ | ✓ | ✓ |
| Multiple statements / loops | ✗ | ✓ | ✓ | ✓ |
| Local variables | ✗ | ✓ | ✓ | ✓ |
if constexpr |
✗ | ✗ | ✓ | ✓ |
try/catch |
✗ | ✗ | ✗ | ✓ |
Dynamic allocation (new) |
✗ | ✗ | ✗ | ✓ |
std::vector / std::string |
✗ | ✗ | ✗ | ✓ |
| Virtual function calls | ✗ | ✗ | ✗ | ✓ |
consteval keyword |
✗ | ✗ | ✗ | ✓ |
constinit keyword |
✗ | ✗ | ✗ | ✓ |
static_assert in function |
✗ | ✓ | ✓ | ✓ |
Recursive constexpr |
✓ | ✓ | ✓ | ✓ |
Common Mistakes
Mistake 1: Assuming constexpr always means compile-time.
constexpr int square(int x) { return x * x; }
int n = 5;
int result = square(n); // Runtime — n is not constexpr
// To FORCE compile-time evaluation:
constexpr int result2 = square(5); // OK: 5 is a constant expression
Mistake 2: Using constexpr with runtime I/O.
constexpr int readAndSquare() {
int x; cin >> x; // ERROR: cin is not constexpr
return x * x;
}
Mistake 3: Forgetting that constexpr member functions are not const by default (C++11).
struct S {
int value;
constexpr int get() const { return value; } // const needed for const objects
constexpr void set(int v) { value = v; } // Non-const constexpr OK in C++14+
};
Mistake 4: constexpr is not inline for variables in headers.
// In a header: use inline constexpr (C++17) to avoid multiple definition
inline constexpr int MAX = 100; // OK in multiple translation units
// Without inline: each TU has its own copy (usually fine, but ODR-sensitive)
Mistake 5: Relying on constexpr for performance without measuring.
// Modern compilers often optimize runtime calls to constants anyway
// Profile before assuming constexpr is necessary for performance
// Use constexpr primarily for correctness guarantees and compile-time validation
Conclusion
constexpr transforms C++ from a language where compile-time computation was possible (via template metaprogramming) to one where it is natural and expressive. Functions look like normal C++ — loops, variables, branches — but execute at compile time when given constant arguments, embedding their results directly into the binary.
The progression from C++11 to C++20 tells a story of expanding expressiveness: from single-return expressions, through full imperative style with loops and local variables, to C++20’s near-complete superset of runtime capabilities including dynamic allocation, std::vector, std::string, virtual functions, and try/catch.
consteval provides compile-time enforcement — when you need a guarantee that a function is never called at runtime. constinit solves the static initialization order fiasco for global variables, ensuring their initial values are set before any dynamic initialization.
Practical uses abound: lookup tables for CRC, sine, logarithm, and hash functions; Fibonacci and factorial precomputation; string hashing for switch-dispatch; prime sieves; compile-time configuration validation. The benefits are real — computation moved to compile time is computation the CPU never has to do, and errors caught at compile time are errors users never see.
Use constexpr wherever you have a function or value that is determined at compile time. Use static_assert with constexpr results as zero-cost correctness checks. Let the compiler do the work — it has a lot of spare cycles while you wait for it to finish.




