constexpr

Compilers Always Did Runtime Work

  • Even back in the old C days, a compiler knew how to add two values

  • Compiler calculates 40+2, and initializes answer with that constant

#include <iostream>

int main()
{
    int answer = 40+2;                                 // <-- addition at compiletime
    std::cout << answer << '\n';
    return 0;
}

⟶ Continue on Godbolt compiler explorer

Motivation: Let Compiler Do More Of This

  • Extract 40+2 into a function

  • ⟶ this turns into a function call, rather than a precalculated constant

  • Would be cool if not

  • ⟶ binary size reduction, startup time

#include <iostream>

int add(int l, int r)
{
    return l+r;
}

int main()
{
    const int a=40, b=2;                               // <-- immutable!
    int answer = add(a, b);
    std::cout << answer << '\n';
    return 0;
}

Even -O3 does not make it go away …

$ g++ -O3 constexpr-add-simple-function.cpp
$ nm --demangle a.out |grep add
00000000004011a0 T add(int, int)

And static? Why Not Use static?

  • static functions are only visible inside their own compilation unit

  • ⟶ compiler has complete freedom to do whatever with it

#include <iostream>

static int add(int l, int r)                           // <-- only visible inside this compilation unit
{
    return l+r;
}

int main()
{
    const int a=40, b=2;
    int answer = add(a, b);
    std::cout << answer << '\n';
    return 0;
}
  • Optimization off ⟶ function call

$ g++ -O0 constexpr-add-simple-function-static.cpp
$ nm --demangle a.out |grep add
0000000000401136 t add(int, int)
  • -O3 ⟶ compiler proves that eliminating add() has no side effects (no outside calls possible) - and eliminates it

$ g++ -O3 constexpr-add-simple-function-static.cpp
$ nm --demangle a.out |grep add

Note

This is not guaranteed! (Results produced with GCC 13)

Enter constexpr

  • Historically, compilers always did such inlining and/or precalculations

  • Not only for trivial functions as 40+2, but for varying degrees of complexity

  • Nontrivial machinery: think about cross compilation, for example

  • It’s just that one cannot count on those optimizations

  • They’re gone with -O0debug and release completely different

  • constexpr

constexpr Functions: Evaluated At Runtime OR Compiletime

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

int main()
{
    const int a=40, b=2;
    int answer = add(a, b);
    std::cout << answer << '\n';
    return 0;
}
$ g++ -O0 constexpr-add-constexpr-function.cpp
$ nm --demangle a.out |grep add
  • Again, this is not guaranteed!

  • Godbolt

    • x86-64 GCC 15: evaluates in constexpr context

    • x86-64 clang 14: generates a call, even though the parameters are immutable

  • ⟶ not much better than static

  • constexpr implies inline though

Forcing Compiletime Evaluation

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

int main()
{
    const int a=40, b=2;                               // <-- immutable
    constexpr int answer = add(a, b);                  // <-- force compiletime evaluation
    std::cout << answer << '\n';
    return 0;
}
  • This is necessary! 🤔

  • consteval fixes this

So What Are The Rules? constexpr Functions Are Dual

Rules

  • If the user wants compiletime evaluation, then they have to say so

    • Only immutable parameters!

  • Runtime evaluation has no constraints - function is just called

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

int main()
{
    const int a=40, b=2;                               // <-- immutable
    constexpr int answer = add(a, b);                  // <-- force compiletime evaluation
    std::cout << answer << '\n';
    return 0;
}
  • If the user wants to supply mutable parameters, they do not request compiletime evaluation

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

int main()
{
    int a=42, b=2;                                     // <-- mutable
    int answer = add(a, b);                            // <-- *don't* force compiletime evaluation
    std::cout << answer << '\n';
    return 0;
}

Uncool: Forcing Compiletime Evaluation Leads To constexpr Result

  • One has to force constexpr context, even when calling with immutable parameters (see here)

  • Assigned-to variable has to be constexpr

  • ⟶ this sucks somehow!

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

int main()
{
    const int a=40, b=1;
    constexpr int answer = add(a, b);
    ++answer;                                          // error: increment of read-only variable ‘answer’
    std::cout << answer << '\n';
    return 0;
}

constexpr Callchains

  • When a constexpr function is evaluated in constexpr context, all its calls have to be constexpr

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

constexpr int add3(int v1, int v2, int v3)
{
    return add(v1, add(v2, v3));
}

int main()
{
    const int a=40, b=2, c=624;
    constexpr int answer = add3(a, b, c);
    std::cout << answer << '\n';
    return 0;
}

Recursion

#include <iostream>

constexpr int add(int l, int r)
{
    return l+r;
}

constexpr int sum(int num)
{
    if (num == 1)
        return num;
    else
        return add(sum(num-1), num);
}

int main()
{
    const int n = 3;
    constexpr int answer = sum(n);
    std::cout << answer << '\n';
    return 0;
}

constexpr Classes

#include <cmath>
#include <iostream>

class point
{
public:
    constexpr point(int x, int y) : _x(x), _y(y) {}
    constexpr double abs() const
    {
        return std::sqrt(_x*_x + _y*_y);               // <-- constexpr
    }
private:
    int _x;
    int _y;
};

int main()
{
    constexpr point p(3, 4);
    constexpr auto abs = p.abs();
    std::cout << abs << '\n';
    // std::cout << p.abs() << '\n';        // defeats constexpr somewhat
    return 0;
}

constexpr Algorithms, constexpr std::vector

  • Rather new: C++20

  • GCC 15 (newest, as of 2025-05-08) does not support this

  • ⟶ apparently, constexpr dynamic allocations are not trivial

#include <algorithm>
#include <vector>
#include <iostream>

int main()
{
    constexpr std::vector numbers{2, 1, 3};
    constexpr std::sort(numbers.begin(), numbers.end());

    for (auto n: numbers)
        std::cout << n << std::endl;

    return 0;
}

Afterword: Cool

  • Compiler has a “virtual machine” of some sort that evaluates C

  • When cross-compiling, that machine’s architecture is the target architecture

  • This is not trivial!