[[constexpr]] tells the C++ compiler that a function or a variable can be evaluated at compile time, so the work happens while the program is being built rather than when it runs. The result is baked into the binary. For code that is a table of constants, a lookup table or a fixed set of rules, that turns a runtime cost into nothing at all.
constexpr functions
A constexpr function may be called with constant arguments at compile time, and the compiler will evaluate it then.
#include
constexpr int measurements_per_hour() {
return 60;
}
constexpr int per_day() {
return measurements_per_hour() * 24;
}
int main() {
std::cout << "per hour: " << measurements_per_hour() << '\n';
std::cout << "per day: " << per_day() << '\n';
static_assert(per_day() == 1440, "a day has 1440 minutes");
return 0;
}The output shows the constants computed before the program started:
per hour: 60
per day: 1440The static_assert is the real payoff. If the assumption is wrong, the program does not compile. In C++11 a constexpr function had to be a single return statement, which is why the functions above are written that way.
constexpr variables
A constexpr variable is a constant whose value the compiler knows exactly. It must be initialised with a constant expression.
#include
constexpr int samples = 16;
constexpr double volts = 3.3;
constexpr int table[samples] = {0}; // the size must be a constant expression
int main() {
std::cout << "samples: " << samples << '\n';
std::cout << "volts: " << volts << '\n';
std::cout << "table: " << table[0] << '\n';
return 0;
}The output shows the values:
samples: 16
volts: 3.3
table: 0Because samples is a constant expression, it can be used as an array size — a plain const int would not be allowed for that in C++11.
constexpr and const are not the same
A const variable cannot be modified after initialisation. A constexpr variable additionally has a value the compiler can compute at compile time.
#include
const int runtime_const = 60; // set at run time
constexpr int compile_time_const = 60; // computed by the compiler
int main() {
std::cout << "runtime const: " << runtime_const << '\n';
std::cout << "compile const: " << compile_time_const << '\n';
// runtime_const cannot be used as an array size in C++11:
// int table[runtime_const];
// But this works, because it is a constant expression:
int table[compile_time_const] = {0};
std::cout << "table size ok: " << (sizeof(table) / sizeof(table[0])) << '\n';
return 0;
}The output shows the difference in what each can be used for:
runtime const: 60
compile const: 60
table size ok: 60A lookup table computed at compile time
The classic use of constexpr is building a table once, at compile time, and reading it at run time.
#include
#include
struct Threshold {
int weight;
int label;
};
constexpr std::array build_thresholds() {
return {{{10, 1}, {25, 2}, {50, 3}, {100, 4}}};
}
constexpr std::array thresholds = build_thresholds();
int label_for(int weight) {
int label = 0;
for (const Threshold& threshold : thresholds) {
if (weight >= threshold.weight) {
label = threshold.label;
}
}
return label;
}
int main() {
std::cout << "label for 30: " << label_for(30) << '\n';
std::cout << "label for 120: " << label_for(120) << '\n';
std::cout << "label for 5: " << label_for(5) << '\n';
return 0;
}The output shows the labels coming from the compile-time table:
label for 30: 2
label for 120: 4
label for 5: 0In C++11 and C++14 the body of a constexpr function had to be a single return statement, so the table was built with a helper. C++14 relaxed the rule to allow loops and branches, and C++20 relaxed it further.
What C++20 changed
C++20 made constexpr considerably more useful.
- Loops and branches in constexpr functions are allowed from C++14 onwards.
- Virtual functions may be called in a constant expression.
dynamic_castandtypeidwork at compile time.std::vectorandstd::stringcan be used in constant expressions (C++20 only; the example above uses plain C++14).- A constexpr function need not be evaluated at compile time; it merely may be.
The practical effect is that more real code can be written as constexpr, which means more bugs are caught at compile time and more work is done by the optimiser.
#include
#include
constexpr std::size_t length_of(const char* text) {
std::size_t length = 0;
while (text[length] != '\0') {
++length;
}
return length;
}
constexpr int total_length(const char* const* items, std::size_t count) {
int total = 0;
for (std::size_t i = 0; i < count; ++i) {
total += static_cast(length_of(items[i]));
}
return total;
}
int main() {
constexpr const char* items[] = {"p-001", "p-002", "p-003"};
std::cout << "total: " << total_length(items, 3) << '\n';
return 0;
}The output shows the array being traversed at compile time:
total: 15When constexpr is worth it
- Tables and lookup data — built once, embedded in the binary.
- Compile-time checks — a
static_assertthat the configuration is valid. - Small computations repeated many times at run time.
- Template-heavy code where the values are needed to instantiate types.
It is not worth it for ordinary arithmetic. The compiler already optimises constant arithmetic, and marking a function constexpr restricts what you can write inside it for very little gain.
Quick reference
constexprfunctions may be evaluated at compile time when called with constant arguments.- A
constexprvariable has a value the compiler knows; aconstone may not. static_assertchecks a compile-time condition and fails the build if it is false.- In C++11 a constexpr function was a single return statement; C++14 allowed loops and branches.
- C++20 allows virtual calls, dynamic_cast and standard containers in constant expressions.
- Use constexpr for tables, compile-time validation and constants — not for ordinary arithmetic.
Try it yourself: write a constexpr function that builds a table of threshold values and a lookup function that reads it, then add a [[static_assert]] that checks the table is sorted. Next, change the table so the assert fails and read the compiler's message. Finally, try using a [[std::vector]] inside a constexpr function and see whether your standard library version supports it.