How C++23 Makes constexpr More Practical (and Why You Should Care)

How C++23 Makes constexpr More Practical (and Why You Should Care)

• 6 min read
• By volker

How C++23 Makes constexpr More Practical (and Why You Should Care)

When I started using modern C++, one of the features which was puzzling me was the constexpr feature. I didn’t really understand what it was for. I knew it let you run some things at compile time, and that was supposed to be good, but I didn’t really know why. I just knew people talked about it like it was a big deal.

Today I know. It’s not just a trick for optimization. It changes how you write code. It gives you a new kind of power: the power to move parts of your program out of runtime entirely. Once you get used to this, it feels natural to push more and more into compile time, not because you’re obsessed with performance, but because it makes your code cleaner, more predictable, and sometimes even simpler.

And with C++23 the constexpr got even easier.

The Old Gotchas

Before C++23, constexpr was more annoying than useful. You could only use it with certain types, and if you got something wrong, the compiler wouldn’t even warn you. You might write a function thinking it would be evaluated at compile time, and it wouldn’t be. But you’d never know unless you went looking for it.

constexpr int my_broken_function() {
    // not a constant expression. Cheerio.
    return std::time(nullptr);
}

That changes dramatically in C++23. Now the compiler will tell you if your constexpr function can’t actually be used as a constant expression. This sounds like a small change, but it’s one of those things that makes constexpr start to feel like a real part of the language, not just a side trick.

The big change is that you can now use constexpr with more types. Before, it had to be a literal type, basically something simple enough for the compiler to understand. Anything with a non-trivial destructor? Nope. Anything fancy with internal state? Also nope. You were done with your opportunities.

Now? As long as destruction can happen during constant evaluation, you’re good. That means things like RAII-style resource wrappers or more complex state machines are suddenly on the table. You can run real logic at compile time.

class Logger {
    int count = 0;
public:
    constexpr void log() { count++; }
    ~Logger() {}
};

constexpr int use_logger() {
    Logger logger;
    logger.log();
    return 42;
}

Yes, this works. Amazing? I think so.

Memory at Compile Time

C++20 already gave us a taste of dynamic memory in constexpr functions. C++23 made it practical. You can now do real work with containers like std::vector and even sort them during compilation.

constexpr auto string_table() {
    std::vector<std::string> table;
    table.reserve(10);
    for (int i = 0; i < 10; ++i) {
        table.push_back("Entry " + std::to_string(i));
    }
    std::sort(table.begin(), table.end());
    return table;
}

constexpr auto TAB = string_table();
static_assert(TAB.size() == 10);

With anything before C++23 this used to be unthinkable. Now it’s a feature.

Standard Library Catching Up

One of the reasons constexpr was awkward before was that the standard library didn’t support it very well. You couldn’t use things like std::sort or std::sin at compile time, which meant writing your own versions of everything.

That’s changing. C++23 makes more functions constexpr. It’s not flashy, but it matters. So today you can write code that looks normal and still runs at compile time. You don’t have to fork the standard library in your own project anymore.

How to Actually Use It

In the following I would like to describe several use-cases where you can apply constexpr.

Compile-Time Constants

The first and most obvious use is defining constants. Not just literal values, but values that are the result of a small computation:

constexpr size_t KB = 1024;
constexpr size_t MB = KB * KB;
constexpr size_t BUFFER_SIZE = 8 * MB;
char buffer[BUFFER_SIZE];

Lookup Tables

Another great use is building tables of precomputed values. Sine tables, transformation tables, anything you don’t want to compute at runtime.

constexpr std::array<double, 360> generate_sin_table() {
    std::array<double, 360> table{};
    for (int i = 0; i < 360; ++i) {
        double rad = i * 3.14159265358979323846 / 180.0;
        table[i] = std::sin(rad);
    }
    return table;
}

constexpr auto SIN = generate_sin_table();

Validation

You can also use constexpr to validate assumptions. This is something most people don’t do but should. If something has to be true, prove it at compile time.

constexpr bool is_power_of_two(unsigned int n) {
    return n != 0 && (n & (n - 1)) == 0;
}

template <unsigned int SIZE>
class Buffer {
    static_assert(is_power_of_two(SIZE), "Size must be a power of two");
};

Buffer<128> valid_buffer

The if constexpr

if constexpr lets you branch based on types or values during compilation. It’s great for writing generic code without making it unreadable. The nice thing here is that the compiler only includes the path that applies. There’s no runtime overhead.

template <typename T>
constexpr T abs_value(T value) {
    if constexpr (std::is_unsigned_v<T>) {
        return value;
    } else {
        return value < 0 ? -value : value;
    }
}

Compile-Time Tests

And finally: testing. Not in the sense of running unit tests before shipping—testing ideas and assumptions as you write them. That can happen at compile time too.

Here’s a simple example: say you want to hardcode a gender guesser for names. You don’t want to mess this up, so you test it:

constexpr const char* get_gender(const std::string_view name) {
    if (name == "Anna" || name == "Maria") return "f";
    if (name == "Lukas" || name == "Peter") return "m";
    return "unknown";
}

constexpr bool test_gender() {
    return std::string_view(get_gender("Anna")) == "f" &&
           std::string_view(get_gender("Peter")) == "m" &&
           std::string_view(get_gender("Alf")) == "unknown";
}

static_assert(test_gender(), "Gender detection failed");

The static_assert is not just for correctness. it’s a form of documentation. It tells anyone reading your code that these are the assumptions everything else rests on.

What About All Those const-Things?

What confused me a lot as I started with modern CPP, was the usage of the keyword const in different context. So here a short overview for those who may be confused as well.

SpecifierDescription
constread-only, but maybe only at runtime
constexprknown at compile time
constevalmust be evaluated at compile time—no exceptions
constinitcompile-time initialized, but not constant

”Volker, Why Does This Matter?”

The real dealmaker about compile-time code is that you can’t fake it. Either it works, or it doesn’t compile. That makes your code more honest. When you rely more on constexpr, you’re forced to think about which parts of your logic are fixed and which ones aren’t. That thinking leaks into everything else you write.

And now, thanks to C++23, it’s not just for toy examples or template magic. You can use it for real work. You should.

Not everything has to happen at runtime. Once you realize that, your code gets better.

And smaller.

And faster.