GitHub - DaveFlater/ConstexpRambles: Rambles on compile-time C++

16 min read Original article ↗

© 2026 David Flater. CC-BY-NC-4.0.

Preramble

C++ is a programming language with another whole programming language inside of it, which is the subset of C++ that is evaluated at compile time.

Since the discovery of template metaprogramming, ISO/IEC JTC 1/SC 22/WG 21 has repeatedly expanded and improved the compile-time capabilities of C++.

The example code in this ramble was processed with g++ -std=gnu++26. Many of the behaviors are implementation-dependent and subject to drastic changes from one version of the C++ language or the g++ compiler to the next.

This is just, like, my opinion, man. If something is outdated, suboptimal, or wrong, open an issue.

Hard mode

Definition:

  • Soft mode is when you precompute things at compile time but still expect to run a compiled executable to complete the task.
  • Hard mode is when the task is completed entirely at compile time. A compiled executable is not a goal. You are writing a script for which the C++ compiler is the interpreter.

Hard mode is weird. It might qualify as an esoteric programming language. It is not a Turing tarpit for the things that it does well, only for the things that are beyond the scope of what has been addressed in the evolution of the language.

Printing is difficult

Getting nice-looking output in hard mode remains a challenge. The general way to print results is using a failed static_assert. If you refer to a number in the assert condition, g++ will tell you what the value was for any kind of ints or floats:

constexpr double m = pow(M_PI, M_PI);
static_assert(m == 0);

assert.cc:5:19: error: static assertion failed
    5 |   static_assert(m == 0);
      |                 ~~^~~~
  • the comparison reduces to ‘(3.6462159607207902e+1 == 0.0)’

But if you want a nicely formatted message, you've got to assemble the string somehow, and std::format isn't constexpr. The following example uses fmtlib:

constexpr auto msg = [&m] () consteval {
  auto msg = std::array<char,40>{};
  fmt::format_to(msg.data(), FMT_COMPILE("The value of m is {}"), m);
  return msg; }();
static_assert(false, msg);

fmt.cc:10:17: error: static assertion failed: The value of m is 36.4621596072079
   10 |   static_assert(false, msg);
      |                 ^~~~~

Control flow is weird

Order of execution is not a thing. The compiler can evaluate statements in any order that results in correct compilation when compilation succeeds.

As a corollary, there is no nice way to abort a computation. You can explicitly disable code that you don't want, but a failed assertion or static error on an earlier line of code does not stop the compiler from processing the lines below it.

Code:

constexpr uint32_t n = 1949220979U, a = 44189U;
static_assert(false, "YOU SHALL NOT PASS!");
#error HALT!
return 0;
throw "STAAAAAHP!!!!";
exit(0);
constexpr uint32_t factor = n / a;
static_assert(!factor, "So anyway, here's the result");

Output:

cantstop.cc:6:4: error: #error HALT!
    6 |   #error HALT!
      |    ^~~~~
cantstop.cc: In function ‘int main(int, char**)’:
cantstop.cc:5:17: error: static assertion failed: YOU SHALL NOT PASS!
    5 |   static_assert(false, "YOU SHALL NOT PASS!");
      |                 ^~~~~
cantstop.cc:11:17: error: static assertion failed: So anyway, here's the result
   11 |   static_assert(!factor, "So anyway, here's the result");
      |                 ^~~~~~~
  • the comparison reduces to ‘(44111 == 0)’

To actually abort, you have to trigger an internal compiler error or segfault, specifically, one that crashes the compiler before the subsequent code completes its mission. Or maybe introduce something that throws off the parsing so that subsequent code is not interpreted.

The concept of "execution" is weird

A totally normal hard mode function cannot be compiled successfully and is never referenced or invoked in any code anywhere, yet nevertheless does just what it says and fulfills its purpose.

Example function:

#include <cmath>
void printStirlingApprox () {
  constexpr double f = sqrt(2*M_PI*N) * pow(N/M_E, N);
  static_assert(f==0, "Stirling approximation to N!");
}

Example "execution:"

bash-5.1$ g++ -DN=20 -c Stirling.cc
Stirling.cc: In function ‘void printStirlingApprox()’:
Stirling.cc:4:18: error: static assertion failed: Stirling approximation to N!
    4 |   static_assert(f==0, "Stirling approximation to N!");
      |                 ~^~~
  • the comparison reduces to ‘(2.4227868467611351e+18 == 0.0)’

The function definition is actually extraneous. The two lines of code do exactly the same thing at global scope. No enclosing function or main program is required.

constexpr versus consteval functions

What's the difference between constexpr and consteval functions? The first order approximation to an answer that you find everywhere on the net is:

  • Constexpr functions may be evaluated at compile time and may be evaluated at run time.
  • Consteval functions must be evaluated at compile time and may not be evaluated at run time.

The devil is in the meaning of the first may be. You might think it's a promise that a constexpr function actually can be evaluated at compile time, but it's not. It's like a street sign that says bagels are 50% off before you enter the bakery and find that the bagels are all gone.

The following example containing a fraudulent claim of constexpr-ness compiles and runs even though the value of errno is undefined at compile time:

#include <cstdio>
#include <cerrno>

constexpr int constFun () {
  return errno;
}

int main ([[maybe_unused]] int argc, [[maybe_unused]] char **argv) {
  printf("%d\n", constFun());
  return 0;
}

You don't get an error until you try to use that value at compile time; for example, by saying static_assert(constFun() == 0):

BrokenPromise.cc: In function ‘int main(int, char**)’:
BrokenPromise.cc:9:28: error: non-constant condition for static assertion
    9 |   static_assert(constFun() == 0);
      |                 ~~~~~~~~~~~^~~~
BrokenPromise.cc:9:25: error: ‘constexpr int constFun()’ called in a constant expression
    9 |   static_assert(constFun() == 0);
      |                 ~~~~~~~~^~
BrokenPromise.cc:4:15: note: ‘constexpr int constFun()’ is not usable as a ‘constexpr’ function because:
    4 | constexpr int constFun () {
      |               ^~~~~~~~
In file included from /Space/gcc/pfx/include/c++/16.0.1/cerrno:47,
                 from BrokenPromise.cc:2:
BrokenPromise.cc:5:10: error: call to non-‘constexpr’ function ‘int* __errno_location()’
    5 |   return errno;
      |          ^~~~~
/usr/include/errno.h:37:13: note: ‘int* __errno_location()’ declared here
   37 | extern int *__errno_location (void) __THROW __attribute_const__;
      |             ^~~~~~~~~~~~~~~~

If the function is declared consteval, you get the error even if the value is not used till run time. But if the function is not used at all, there is once again no error. Schrödinger's cat's function is both valid and invalid until someone tries to call it.

if constexpr versus if consteval

The difference between if constexpr and if consteval is not subtle. if constexpr tests a parenthesized condition at compile time. if consteval has no parenthesized condition: it is just true at compile time and false at run time.

Annoyances

"is not a constant expression" sometimes just means "ew, I don't like that value"

constexpr double a = 1e+1000;
constexpr double b = exp(2.5);
constexpr double c = exp(1000.0);

tryit.cc:2:1: warning: floating constant exceeds range of ‘double’ [-Woverflow]
    2 | constexpr double a = 1e+1000;
      | ^~~~~~~~~
tryit.cc:4:25: error: ‘exp(1.0e+3)’ is not a constant expression
    4 | constexpr double c = exp(1000.0);
      |                      ~~~^~~~~~~~

So overflowing a double is constexpr, and the exp function is constexpr, but if a double overflows inside of the exp function, that is not constexpr.

Function parameter is not constexpr (says the compiler)

It's not legal syntax to put constexpr on a function parameter. It is up to the compiler to decide whether the function parameter is constexpr or not. It does so in an inscrutable, apparently arbitrary and capricious, plausibly malicious manner. Computer says no.

Pop quiz: which of these four functions is invalid?

consteval uint32_t example1 (uint32_t n) {
  if (!n) return 32U; else return 64U;
}

consteval uint32_t example2 (uint32_t n) {
  if constexpr (!n) return 32U; else return 64U;
}

template <uint32_t n> consteval uint32_t example3 () {
  if (!n) return 32U; else return 64U;
}

template <uint32_t n> consteval uint32_t example4 () {
  if constexpr (!n) return 32U; else return 64U;
}

Answer: example2. Only example2.

if_constexpr.cc: In function ‘consteval uint32_t example2(uint32_t)’:
if_constexpr.cc:9:17: error: ‘n’ is not a constant expression
    9 |   if constexpr (!n) return 32U; else return 64U;
      |                 ^~

More examples will be provided below in the tier list of compile-time iteration methods.

Template parameter rules

When the compiler refuses to believe that a function parameter is constexpr, your first recourse is to convert it into a template parameter, but the rules on what is valid as a non-type template parameter are restrictive. For example, the C++ complex number class doesn't work:

Class.cc:2:32: error: ‘std::complex<double>’ is not a valid type for a template non-type parameter because it is not structural
    2 | template <std::complex<double> s> void foo () {}
      |                                ^
In file included from Class.cc:1:
/Space/gcc/pfx/include/c++/16.0.1/complex:1748:17: note: ‘std::complex<double>::_M_value’ is not public
 1748 |       _ComplexT _M_value;
      |                 ^~~~~~~~

And the plain old C type doesn't work either:

NotClass.cc:2:27: error: ‘__complex__ double’ is not a valid type for a template non-type parameter
    2 | template <double _Complex s> void foo () {}
      |                           ^

But you can pass the real and imaginary parts separately as doubles and reassemble a constexpr complex on the other side. The template signature is clearly the bottleneck.

Standard library functions that could be constexpr but aren't

For example, std::abs<complex> is not constexpr:

constexpr std::complex<double> a(1.0, 1.0);
constexpr double b = std::abs(a);

tryit.cc:5:30: error: call to non-‘constexpr’ function ‘_Tp std::abs(const complex<_Tp>&) [with _Tp = double]’
    5 | constexpr double b = std::abs(a);
      |                      ~~~~~~~~^~~
In file included from tryit.cc:2:
/Space/gcc/pfx/include/c++/16.0.1/complex:964:5: note: ‘_Tp std::abs(const complex<_Tp>&) [with _Tp = double]’ declared here
  964 |     abs(const complex<_Tp>& __z) { return __complex_abs(__z.__rep()); }
      |     ^~~

It's trivial to implement a constexpr replacement, so there's no good reason for it:

constexpr double constabs (std::complex<double> s) {
  return std::sqrt(s.real()*s.real() + s.imag()*s.imag());
}
static_assert(constabs(a) == 0);

tryit.cc:10:27: error: static assertion failed
   10 | static_assert(constabs(a) == 0);
      |               ~~~~~~~~~~~~^~~~
  • the comparison reduces to ‘(1.4142135623730951e+0 == 0.0)’

Tier list of compile-time iteration methods

Example task

Given double s = 1 as an initial value, the example task is to iterate s = constexprFunction(s, x) for uint32_t x from 1 to n as verbatim as possible. You lose points if you have to convert it to a recursive form, add helper functions, change the data type of x, or start the counter at 0.

S: plain old for loop

consteval double example (uint32_t n) {
  double s = 1;
  for (uint32_t x=1; x<=n; ++x)
    s = constexprFunction(s, x);
  return s;
}

Can't beat the direct approach. When it works, it works.

A+: std::ranges::for_each

consteval double example (uint32_t n) {
  double s = 1;
  // std::views::iota runs from begin to end-1
  std::ranges::for_each(std::views::iota(1U, n+1),
                        [&s] (uint32_t x) { s = constexprFunction(s, x); });
  return s;
}

So we're using a standard algorithm instead of a plain old for loop. The syntax is more verbose, but otherwise it's equivalent.

A: hana::for_each

Using the Boost Hana library:

#include <boost/hana.hpp>
using namespace boost;

template <uint32_t n> consteval double example () {
  double s = 1;
  // hana::range_c runs from begin to end-1
  hana::for_each(hana::range_c<uint32_t, 1, n+1>,
                 [&s] (uint32_t x) { s = constexprFunction(s, x); });
  return s;
}

Still a straightforward and faithful implementation of the loop, just more templatey. It's at this point that n being a function parameter instead of a template parameter starts to result in "error: ‘n’ is not a constant expression" even though it's exactly as constexpr as it was a moment ago.

A−: expansion statement

template <uint32_t n> consteval double example () {
  double s = 1;
  // std::views::iota runs from begin to end-1
  template for (constexpr uint32_t x: std::views::iota(1U, n+1)) {
    s = constexprFunction(s, x);
  }
  return s;
}

The motivation given for this construct is that you don't want the template overhead or the lambda function of the preceding for_each implementations. You just want to iterate, dammit. If there's a return statement in the loop body, you want to return from the function, not just from the lambda.

That's relatable, but then this is some weird syntax. If it avoids templates, why does it use the template keyword? If you prohibit putting constexpr on a function parameter, how can you explain putting it on a loop variable?

There's also the practical issue that this is new in C++26, and g++ version 16.0.1 20260225 (experimental) dies with an internal compiler error on this example.

Relevant g++ soft limits: -fconstexpr-loop-limit, -fconstexpr-ops-limit, et al.

B+: mp11::mp_for_each

Using the Boost mp11 library:

#include <boost/mp11.hpp>
using namespace boost::mp11;

template <uint32_t n> using uint32 = std::integral_constant<uint32_t, n>;

template <uint32_t n> consteval double example () {
  double s = 1;
  // mp_iota first parameter is number of iterations, second parameter is
  // begin (added in v 1.83.0, optional, default 0).
  mp_for_each<mp_iota<uint32<n>, uint32<1U>>>(
    [&s] (uint32_t x) { s = constexprFunction(s, x); });
  return s;
}

mp11::mp_for_each is just an earlier, C++11 implementation of for_each. Roughly equivalent to hana::for_each, but even more templatey and another step down on syntax and ease of use.

B: std::ranges::fold_left

consteval double example (uint32_t n) {
  // std::views::iota runs from begin to end-1
  return std::ranges::fold_left(std::views::iota(1U, n+1),
                                1.0, constexprFunction);
}

Now we are changing the structure of the example from simple repetition to nested function invocations. It's equivalent, but it's not verbatim what was asked for. Having said that, it's a really clean implementation.

B−: hana::fold

#include <boost/hana.hpp>
using namespace boost;

template <uint32_t n> consteval double example () {
  // hana::range_c runs from begin to end-1
  return hana::fold(hana::range_c<uint32_t, 1, n+1>,
                    1.0, constexprFunction);
}

Very similar to std::ranges::fold_left except that the "error: ‘n’ is not a constant expression" problem comes back if n is passed as a function parameter.

C+: template recursion terminated with if constexpr

template <uint32_t x> consteval double example () {
  if constexpr (x == 0)
    return 1.0;
  else
    return constexprFunction(example<x-1>(), x);
}

Once again, we have changed the structure of the example to fit the iteration method.

Relevant g++ soft limit: -ftemplate-depth (default 900). This limit can be raised a lot, but g++ starts segfaulting eventually.

C: template recursion terminated with template specialization

You can go with this:

template <uint32_t x> consteval double example () {
  return constexprFunction(example<x-1>(), x);
}

template <> consteval double example <0U> () {
  return 1.0;
}

Or you can go with that:

template <uint32_t x> struct example {
  static constexpr double val = constexprFunction(example<x-1>::val, x);
};

template <> struct example <0U> {
  static constexpr double val = 1.0;
};

For this example, it makes no difference whether you use a function or a struct. However, if additional template parameters were required, struct would be the only option: partial template specialization is defined for structs but not for functions.

The if constexpr syntax is cleaner, but g++ goes longer without segfaulting with template specialization instead.

C−: fold expression

template <uint32_t n> consteval double example () {
  double s = 1;
  // make_integer_sequence inflexibly generates 0 to end-1
  [&s] <uint32_t... x> (std::integer_sequence<uint32_t, x...>) {
      (..., (s = constexprFunction(s, x+1)));
    }(std::make_integer_sequence<uint32_t, n>{});
  return s;
}

Three ellipses, two integer_sequences, and an empty pair of curly brackets that isn't optional. Yuck. Despite the success of other kinds of folds, this one has "wrong tool for the job" smell all over it. It gets a C− because it gets the job done and it supports more iterations than preprocessor unrolling does, but there is so much to hate about it.

A fold expression needs a pack (the elided thing). The lambda function is just to get one of those, a function parameter pack. I couldn't get it to work with a pack from a structured binding.

The fold expression syntax works only with 32 canonical binary operators. The operator supplied here is the comma, which does nothing but ensure that the iterations will happen in sequential order. With operator overloading, maybe a canonical operator could be redefined to constexprFunction, but using the comma gets closer to the verbatim example anyway.

Instead of a range, you have to use an integer_sequence. Now I'm passing a bajillion parameters to the extra function that I didn't want. The time and memory needed to compile balloon worse than they do for template recursion, and then the compiler segfaults.

integer_sequence brings its own misfeatures to the party. Every way to get an integer_sequence that starts at 1 is nonobvious. It should be trivial!

D: preprocessor unrolling

#include <boost/preprocessor/repetition/repeat_from_to.hpp>
#include <boost/preprocessor/arithmetic/add.hpp>

#define loopbody(z, x, data) s = constexprFunction(s, x);

consteval double example () {
  double s = 1;
  // BOOST_PP_REPEAT_FROM_TO runs from begin to end-1
  BOOST_PP_REPEAT_FROM_TO(1, BOOST_PP_ADD(N, 1), loopbody, unused)
  return s;
}

This method has two major limitations: one, the Boost Preprocessor library (C++03) has a hard limit of 1024 iterations (which has to be raised from a default of 256); two, the loop bounds have to be preprocessor integer constants (#define N 10). If neither of those is a problem, this is a solid approach.

D−: explicit unrolling

consteval double example (uint32_t n) {
  double s = 1;
  if (n < 1U) return s; s = constexprFunction(s, 1U);
  if (n < 2U) return s; s = constexprFunction(s, 2U);
  if (n < 3U) return s; s = constexprFunction(s, 3U);
  if (n < 4U) return s; s = constexprFunction(s, 4U);
  if (n < 5U) return s; s = constexprFunction(s, 5U);
  // ... continue to maximum supported n
  return s;
}

It's straightforward and it meets the requirements, but nobody wants a million or billion line header file.

F

When the iteration method itself breaks consteval, that's a fail.

The Boost Foreach library is C++03. Constexpr was added in C++11, so it has an excuse.

consteval uint32_t fail () {
  constexpr uint32_t arr[] = {1U, 2U, 3U};
  uint32_t s = 0U;
  BOOST_FOREACH(uint32_t x, arr) { s += x; }
  return s;
}

BoostForeach.cc:9:3: error: call to non-‘constexpr’ function ‘boost::foreach_detail_::auto_any<T*> boost::foreach_detail_::contain(T&, mpl_::false_*) [with T = const unsigned int [3]; mpl_::false_ = mpl_::bool_<false>]’
    9 |   BOOST_FOREACH(uint32_t x, arr) { s += x; }
      |   ^~~~~~~~~~~~~

The for_each in the Boost MPL library (also C++03) is described in the documentation as a "runtime algorithm." Unsurprisingly, it doesn't work at compile time:

struct constexprFunction {
  template <typename U> constexpr void operator() (U x) {}
};

consteval uint32_t fail () {
  // mpl::range_c runs from begin to end-1
  mpl::for_each< mpl::range_c<uint32_t, 1U, 4U> >( constexprFunction() );
  return 0U;
}

MPLForeach.cc:13:50: error: call to non-‘constexpr’ function ‘void boost::mpl::for_each(F, Sequence*) [with Sequence = range_c<unsigned int, 1, 4>; F = constexprFunction]’
   13 |   mpl::for_each< mpl::range_c<uint32_t, 1U, 4U> >( constexprFunction() );
      |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~

I could not figure out the syntax to apply mp11::mp_fold or mpl::fold to the example problem, so they both get an F.