C++ Part I · Module 21 · Modern C++ Type and Utility Tools
constexpr: Computing at Compile Time
A computation that depends only on values the compiler can see does not have to happen when the program runs. constexpr is how you say so, and the chapter's frozen example is the kind of code this matters for most: a helper that builds a register bit-mask from a position and a width, of exactly the shape found in every hardware-facing codebase. The chapter measures what constexpr buys. The value becomes usable where the language requires a constant — an array bound, a template argument, an enumerator — which is a capability and not an optimisation. It becomes checkable by static_assert, so a wrong mask is a build failure rather than a wrong register write. And where the compiler evaluates it, the run-time cost is zero. It also measures the distinction the frozen purpose asks for, which is routinely blurred: const is a promise that a value will not change, while constexpr is a requirement that it be known at compile time, and the chapter shows the one-line program where the first compiles and the second does not. Then there is the finding this chapter did not expect. Subjecting the curriculum's own mask function to static_assert at every width exposed a latent undefined-behaviour bug at the full thirty-two-bit case — a bug that at run time returns a plausible, silently wrong answer with no warning from any flag, and that constexpr evaluation refuses outright while naming the cause precisely.
Advanced19 min readC++Part IFundamentalsModern C++
Part I · Module 21 · Chapter 21.2 · constexpr: Computing at Compile Time
1. The Frozen Example
constexpr std::uint32_t mask(unsigned pos, unsigned width) {
return ((1u << width) - 1u) << pos;
}That is the curriculum's own example for this chapter and it is worth reading closely, because it is the shape of a helper that appears in essentially every codebase that touches hardware. Build width ones, shift them up to pos, and you have a mask for a field.
constexpr on a function means: this function is permitted to be evaluated at compile time, when its arguments are themselves compile-time constants. It does not mean it will always be, and §5 measures the same function running at run time.
What it does mean is that the results can be checked:
static_assert(mask(0, 4) == 0x0000000Fu, "low nibble");
static_assert(mask(4, 4) == 0x000000F0u, "second nibble");
static_assert(mask(8, 8) == 0x0000FF00u, "byte 1");static_assert(mask(0,4)==0xF) and two more: all passed at compile time
mask(0,4)=0x0000000F mask(4,4)=0x000000F0 mask(8,8)=0x0000FF00Three assertions, evaluated by the compiler, costing nothing at run time and failing the build if the arithmetic is wrong. §3 is what happened when I extended that check to every width.
2. A Constant Expression Is a Capability
The first thing constexpr buys is not speed. It is the ability to use a computed value where the language requires a constant:
int arr[mask(0, 3)]; // an array bound
std::array<int, mask(0, 4)> a{}; // a template argument
enum E : std::uint32_t { AllFour = mask(0, 4) }; // an enumeratorint arr[mask(0,3)] -> 7 elements
std::array<int, mask(0,4)> -> 15 elements
enum { AllFour = mask(0,4) } -> 0xFNone of those three contexts accepts a run-time value. Before constexpr, the only way to compute a value usable there was the preprocessor — #define MASK(p,w) ((((1u)<<(w))-1u)<<(p)) — which has no types, no scope, no overloading and no diagnostics, and which Chapter 1.4 §8 lists among the habits to replace. constexpr gives you a real function, with parameter types the compiler checks, that works in all the same places.
This is also Chapter 18.5's non-type template parameter made usable: std::array<int, mask(0,4)> is a std::array whose size came out of a function call, and Chapter 18.5 §4 measured the exact-storage consequence.
3. The Frozen Example Has a Bug
While writing §1 I tried to extend the assertions to the full-width case, and the build failed:
static_assert(mask(0, 32) == 0xFFFFFFFFu, "full-width mask");maskub.cpp:5:15: error: static assertion expression is not an integral constant expression
5 | static_assert(mask(0, 32) == 0xFFFFFFFFu, "full-width mask");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
maskub.cpp:3:15: note: shift count 32 >= width of type 'unsigned int' (32 bits)
3 | return ((1u << width) - 1u) << pos;
| ^
maskub.cpp:5:15: note: in call to 'mask(0, 32)'One error, and the cause is named exactly: 1u is a 32-bit unsigned int, and shifting a 32-bit value by 32 is undefined behaviour. A constant expression may not contain undefined behaviour, so the compiler refuses to produce a value — and tells you which shift, in which function, from which call.
That is not a defect in my transcription; it is a property of the idiom. Now the same expression at run time, where width comes from a variable the optimiser cannot see:
A. widths 1..31 are fine
mask(0,1)=0x00000001 mask(0,8)=0x000000FF mask(0,31)=0x7FFFFFFF
B. width == 32 shifts a 32-bit value by 32 -- UNDEFINED
at RUN time mask(0,32) = 0x00000000 (whatever the hardware did)
the correct answer is 0xFFFFFFFFZero, where the correct answer is 0xFFFFFFFF, with zero compile warnings. A mask of all-zeroes means "no bits", so a read-modify-write using it would silently preserve the field it was supposed to clear. That is precisely the class of hardware bug that takes a week to find.
-fsanitize=shift does catch the run-time form:
maskedge.cpp:6:15: runtime error: shift exponent 32 is too large for 32-bit
type 'unsigned int'— and, as in Chapter 20.7 §5, it reports and then exits 0, so a test checking only the status still passes.
The fix is to handle the full width explicitly:
constexpr std::uint32_t maskSafe(unsigned pos, unsigned width) {
return width >= 32u ? (0xFFFFFFFFu << pos)
: (std::uint32_t)(((1u << width) - 1u) << pos);
}maskSafe(0,32) = 0xFFFFFFFF maskSafe(0,8) = 0x000000FF
static_assert on maskSafe at 32, 8 and 8@8: all passedDoing the arithmetic in a 64-bit type (1ull << width) is the other standard fix and is often cleaner.
The point of the section is not the bug. It is that constexpr plus static_assert found it, at build time, in a function that had already been written down as a reference example — which is a stronger argument for the technique than any amount of advocacy.
4. const and constexpr Are Different Promises
This is the distinction the frozen purpose asks for.
const says this will not change after initialisation. It says nothing about when the value is known.
constexpr says this must be known at compile time. It is a requirement the compiler enforces.
std::uint32_t fromArgv(int n) { return (std::uint32_t)n; }
const std::uint32_t a = fromArgv(argc); // fine
constexpr std::uint32_t b = fromArgv(argc); // ERRORcexpr_bad.cpp:7:29: error: constexpr variable 'b' must be initialized by a
constant expression
cexpr_bad.cpp:7:42: note: function parameter 'argc' with unknown value cannot
be used in a constant expressionOne error, and the note names argc specifically. The const version is perfectly legal: it is a variable that will not be reassigned, initialised from whatever the program computed.
The corollary is the one that matters in practice. A const variable may happen to be a constant expression — if its initialiser is one, and its type is an integral or enumeration type, it can be used as an array bound. A constexpr variable always can. So:
const std::uint32_t a = mask(0, 4);
constexpr std::uint32_t b = mask(0, 4);
std::array<int, b> ok{}; // guaranteed to work
std::array<int, a> also{}; // works here, but by luck of the type and initialiserWrite constexpr when you mean "the compiler must know this", and const when you mean "nobody may change this". They are frequently both true, and then constexpr is the stronger and better statement.
On a member function, const and constexpr are orthogonal and both may appear — constexpr meaning the call can be evaluated at compile time, const meaning it does not modify *this, which is Chapter 11.7's subject.
5. The Same Function at Run Time
unsigned p = 8, w = 8;
sink(p); sink(w); // hide the values from the optimiser
std::printf("mask(p,w) = 0x%08X\n", mask(p, w));pos=8 width=8
mask(p,w) = 0x0000FF00 -- computed at RUN time, same function
constexpr PERMITS compile-time evaluation; it does not require it.This is the property people most often get wrong. constexpr on a function is permission, not obligation. Called with constants it may be folded; called with run-time values it is an ordinary function. There is one function, and it works in both worlds — which is the whole reason it is better than the macro it replaces.
If you need to require compile-time evaluation, C++20 adds consteval, which makes a run-time call a compile error. This compiler reports __cpp_consteval = 202211, so it is available; consteval is the right tool for a function that would be meaningless or unsafe at run time.
And constexpr on a variable is an obligation — §4's measurement — which is why the keyword means subtly different things in the two positions.
6. What a constexpr Function May Contain
The rules were restrictive in C++11 and have relaxed at every standard since, which matters because advice written against C++11 is now wrong.
C++11 allowed essentially one return statement. No loops, no local variables, no branches except the conditional operator. That is why the frozen mask is written as a single expression.
C++14 allowed local variables, loops, multiple statements and if. This is the change that made constexpr generally useful:
constexpr unsigned popcount(std::uint32_t v)
{
unsigned n = 0;
while (v) { n += (v & 1u); v >>= 1; }
return n;
}
static_assert(popcount(0xFFu) == 8, "");
static_assert(popcount(mask(4, 12)) == 12, "");popcount(0xFF)=8 popcount(mask(4,12))=12 -- both at compile timeNote the second assertion composes the two functions: a constexpr function may call another, and the whole chain is evaluated by the compiler.
C++17 added if constexpr (§8) and made lambdas implicitly constexpr where possible.
C++20 allowed try/catch, dynamic allocation that does not escape the evaluation, virtual calls, and std::vector/std::string in constant expressions — which is a large expansion.
What remains forbidden at every level: anything whose result the compiler cannot know. Reading a global that is not itself constant, calling a non-constexpr function, undefined behaviour of any kind — which is §3's error.
7. A Compile-Time Table
constexpr std::array<std::uint32_t, 8> byteMasks()
{
std::array<std::uint32_t, 8> a{};
for (unsigned i = 0; i < 8; ++i) a[i] = mask(i * 4, 4);
return a;
}
constexpr auto t = byteMasks();
static_assert(t[0] == 0x0000000Fu, "");
static_assert(t[7] == 0xF0000000u, "");byteMasks() built at compile time: 0000000F 000000F0 00000F00 0000F000
000F0000 00F00000 0F000000 F0000000
static_assert on t[0] and t[7] passed, so the array is a constant.A lookup table computed by the compiler and placed in read-only data, with its contents asserted. This replaces three older approaches: a hand-written literal array that nobody can check, a table built at startup by a function that runs before main, and a macro-generated monstrosity.
std::array works here where std::vector would not in C++17, because the array's storage is part of the object — Chapter 19.2 §3's measurement — and so can live in a constant. C++20 relaxed this for vector provided the allocation does not escape.
8. if constexpr Is a Different Feature
The keyword appears in both, and they do different things. constexpr on a function or variable is about when a value is computed. if constexpr is about which code exists:
auto describe = [](auto v) {
if constexpr (std::is_integral_v<decltype(v)>) return "integral";
else if constexpr (std::is_floating_point_v<decltype(v)>) return "floating";
else return "other";
};describe(1)=integral describe(1.5)=floating describe("x")=other
the untaken branches are NOT compiled -- 18.8 §9 measured that.The untaken branches are discarded before compilation, so each may contain code that would be invalid for the other types — which is exactly what Chapter 18.8 §9 measured, compiling three mutually-incompatible branches in one function. A plain if compiles all branches and fails.
So: if constexpr for selecting code by type, constexpr for computing values early. They compose, as describe shows, and they are not the same tool.
9. What It Costs
Three costs, all modest, all worth naming.
Build time. The compiler is now executing your code. A deeply recursive constexpr computation, or a large table, measurably slows compilation, and implementations impose limits on evaluation steps and recursion depth that a runaway computation will hit.
Constraints on the body. §6's list. Writing a function to be constexpr sometimes means writing it differently from how you would otherwise — the frozen mask being a single expression is a C++11 artefact.
Code that must work in both worlds. A constexpr function cannot call a non-constexpr one, so marking a function constexpr propagates a requirement down everything it calls. That is usually fine and occasionally awkward.
There is no run-time cost. Where the compiler evaluated the expression, the value is in the instruction stream or in read-only data and nothing is computed when the program runs.
10. What to Actually Do
- Mark a function
constexprwhen its result could be useful as a constant. It costs nothing when called at run time. static_assertthe boundaries of everyconstexprfunction — the zero case, the full-width case, the maximum index. That is what found §3's bug.- Write
constexpron a variable when the compiler must know the value,constwhen nobody may change it. Both, when both are true. - Prefer a
constexprfunction to a function-like macro. Types, scope, overloading and diagnostics, all of which the macro lacks. - Build lookup tables with a
constexprfunction returningstd::array, and assert the endpoints. - Do the arithmetic in a wide enough type.
1ull << widthavoids §3's bug entirely. - Reach for
consteval(C++20) when run-time evaluation would be meaningless or unsafe. - Do not confuse
if constexprwithconstexpr— one selects code, the other computes values. - Watch build time on recursive or large computations.
11. Mistakes Worth Making Once
Shifting by the full width of the type. Measured: a silently wrong 0x00000000 at run time, a hard error as a constant, and the standard register-mask idiom contains it.
Expecting constexpr on a function to force compile-time evaluation. It is permission. consteval is the obligation.
Using const where constexpr was needed. The const version compiles and then fails the first time you try to use it as a template argument from a non-integral type.
Asserting only the easy cases. Three passing assertions in §1 found nothing; the fourth found the bug.
Calling a non-constexpr function from a constexpr one and being surprised the whole thing is no longer usable in a constant expression.
A constexpr function reading a non-constexpr global. Same problem, less obvious.
Assuming -Wall will warn about the shift. Measured: zero warnings. -fsanitize=shift catches it at run time, for the inputs you run.
12. Interview Relevance
The frozen curriculum marks this chapter High, and the const-versus-constexpr question is the standard opener.
"What is the difference between const and constexpr?" const promises the value will not change; constexpr requires it to be known at compile time. The strong answer adds that on a variable constexpr is an obligation and on a function it is permission, and gives the f(argc) example.
"Does constexpr guarantee compile-time evaluation?" No — not on a function. Called with run-time arguments it is an ordinary function. consteval in C++20 is the guarantee.
"What can a constexpr function do?" Depends on the standard: one return in C++11, loops and locals in C++14, and in C++20 even allocation that does not escape. Knowing it changed is better than reciting one version's rules.
"Why prefer it to a macro?" Types, scope, overloading, diagnostics, and it works in both constant and run-time contexts.
"How would you test a constexpr function?" static_assert at the boundaries, which runs on every build at no run-time cost. Having found a real bug that way is the best possible version of this answer.
"Can a constant expression contain undefined behaviour?" No — and that is what makes the compiler's constant evaluator a build-time UB detector.
13. What You Should Be Able to Say Now
- That
constexpron a function is permission and on a variable is an obligation. - That
constmeans will-not-change and says nothing about when the value is known. - That a constant expression may be used as an array bound, a template argument or an enumerator — and measured 7, 15 and 0xF.
- That the frozen
maskreturns 0x00000000 at run time for width 32 with zero warnings, and that the same call is a hard error as a constant, naming the shift count. - That
-fsanitize=shiftcatches the run-time form and exits 0 anyway. - That
static_assertover aconstexprfunction is a testing technique, and that boundaries are what to assert. - That the body rules relaxed in C++14, C++17 and C++20.
- That
if constexprselects code andconstexprcomputes values.
14. Exercises
Exercise 1 — reproduce the bug. Write the frozen mask and assert it at widths 1, 8, 31 and 32. Record which assertion fails and the exact diagnostic.
Exercise 2 — two fixes. Fix it with the ternary and with 64-bit arithmetic. Assert both at every width from 0 to 32, and state which fix you prefer and why.
Exercise 3 — width 0. Assert mask(0, 0). Work out what it should be, what it is, and whether that is a bug.
Exercise 4 — const versus constexpr. Write the f(argc) pair and read both outcomes. Then find a const variable that works as an array bound and explain exactly why.
Exercise 5 — the table. Build a 256-entry CRC or parity table with a constexpr function and assert four entries. Then check the generated binary to confirm the table is in read-only data.
Exercise 6 — build time. Write a recursive constexpr function and increase the depth until the compiler complains. Report the limit and the flag that changes it.
Exercise 7 — permission, not obligation. Call one constexpr function with constants and with run-time values, and show from the disassembly that one call vanished and the other did not.
Exercise 8 — consteval. Rewrite one function as consteval and show that a run-time call becomes a compile error.
15. Summary
constexpron a function is permission to evaluate at compile time; on a variable it is an obligation.constmeans will-not-change; measured,const T a = f(argc);compiles andconstexpr T b = f(argc);is one error namingargc.- A constant expression is a capability: measured as an array bound (7), a template argument (15) and an enumerator (0xF), replacing the macro that had no types or scope.
- Measured: the frozen
mask()has undefined behaviour atwidth == 32. At run time it returned 0x00000000 for a correct answer of 0xFFFFFFFF, with 0 compile warnings; as a constant it is a hard error naming "shift count 32 >= width of type 'unsigned int'";-fsanitize=shiftcatches the run-time form and exits 0. - A constant expression may not contain undefined behaviour, which makes the constant evaluator a build-time UB detector.
- So assert the boundaries, not the middle. Three passing assertions found nothing; the fourth found the bug.
- Measured: the C++14 relaxation works — a
whileloop in aconstexprfunction, asserted through a composed call. - Measured: a compile-time table, eight entries, asserted at both ends.
if constexprselects code;constexprcomputes values. Different features, shared keyword.- The costs are build time, body constraints and propagation — never run-time cost.
The two chapters so far have named a type and computed a value, both at compile time and both without changing what the program does when it runs. Chapter 21.3 takes the third compile-time question: not naming a type you know, but expressing the type of an expression you cannot name — which is what a generic interface needs when its return type depends on its arguments. It also contains the smallest trap in the module, where adding one pair of parentheses changes a return type from a value to a reference, and the reference is to a local variable that has already been destroyed.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
