Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static_assert(mask(0, 4) == 0x0000000Fu, "low nibble");
static_assert(mask(4, 4) == 0x000000F0u, "second nibble");
static_assert(mask(8, 8) == 0x0000FF00u, "byte 1");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)=0x0000FF00

Three 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 enumerator
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int arr[mask(0,3)] -> 7 elements
std::array<int, mask(0,4)> -> 15 elements
enum { AllFour = mask(0,4) } -> 0xF

None 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static_assert(mask(0, 32) == 0xFFFFFFFFu, "full-width mask");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 0xFFFFFFFF

Zero, 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
constexpr std::uint32_t maskSafe(unsigned pos, unsigned width) {
  return width >= 32u ? (0xFFFFFFFFu << pos)
                      : (std::uint32_t)(((1u << width) - 1u) << pos);
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
maskSafe(0,32) = 0xFFFFFFFF   maskSafe(0,8) = 0x000000FF
static_assert on maskSafe at 32, 8 and 8@8: all passed

Doing 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);   // ERROR
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cexpr_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 expression

One 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 initialiser

Write 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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));
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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, "");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
popcount(0xFF)=8 popcount(mask(4,12))=12 -- both at compile time

Note 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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, "");
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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";
};
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 constexpr when its result could be useful as a constant. It costs nothing when called at run time.
  • static_assert the boundaries of every constexpr function — the zero case, the full-width case, the maximum index. That is what found §3's bug.
  • Write constexpr on a variable when the compiler must know the value, const when nobody may change it. Both, when both are true.
  • Prefer a constexpr function to a function-like macro. Types, scope, overloading and diagnostics, all of which the macro lacks.
  • Build lookup tables with a constexpr function returning std::array, and assert the endpoints.
  • Do the arithmetic in a wide enough type. 1ull << width avoids §3's bug entirely.
  • Reach for consteval (C++20) when run-time evaluation would be meaningless or unsafe.
  • Do not confuse if constexpr with constexpr — 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 constexpr on a function is permission and on a variable is an obligation.
  • That const means 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 mask returns 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=shift catches the run-time form and exits 0 anyway.
  • That static_assert over a constexpr function 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 constexpr selects code and constexpr computes 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

  • constexpr on a function is permission to evaluate at compile time; on a variable it is an obligation.
  • const means will-not-change; measured, const T a = f(argc); compiles and constexpr T b = f(argc); is one error naming argc.
  • 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 at width == 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=shift catches 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 while loop in a constexpr function, asserted through a composed call.
  • Measured: a compile-time table, eight entries, asserted at both ends.
  • if constexpr selects code; constexpr computes 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.