Sitemap
Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Evolution of C++ constexpr: From C++11 to C++20 Compile-Time Power

Trace the journey of compile-time programming from the strict limits of C++11 to the near-unrestricted power of C++20.

8 min readJan 15, 2026

--

Press enter or click to view image in full size

In my previous posts constexpr variable and constexpr function i have explained in detail about C++11 constexpr . This post covers the evolution of constexprsince their introduction in C++11 across later standards.

C++11: Restricted constexpr function:

constexpr function introduced in C++11 were severely limited with the following restriction:

  1. Must contain only a single return statement (Major Restriction)
  2. No local variables allowed
  3. No loops (for, while) (Forced us to use recursion)
  4. No if statements (only ternary operator ?:)
  5. Function body must be a single expression

C++14: Relaxed constexpr function:

The following capabilities were added to constexpr function in C++14.

  1. Multiple statements allowed
  2. Local variables allowed
  3. Loops (for, while, do-while) (much needed enhancement for TMP)
  4. If-else statements
  5. Multiple return statements (much needed enhancement for TMP)
  6. switch statements
  7. Modify local variables
#include <iostream>

// C++14: Multiple statements and local variables
constexpr int sum_to_n(int n) {
int sum = 0; // Local variable allowed!

for (int i = 1; i <= n; ++i) { // Loop allowed!
sum += i;
}

return sum; // Multiple statements allowed!
}

int main() {
constexpr int result = sum_to_n(100);
std::cout << "Sum(1..100) = " << result << std::endl;
return 0;
}
#include <iostream>

// C++14: Factorial with loop instead of recursion
constexpr int factorial(int n) {
int result = 1;
for (int i = 2; i <= n; ++i) {
result *= i;
}
return result;
}

// C++14: Fibonacci with loop
constexpr int fibonacci(int n) {
if (n <= 1) return n;

int prev = 0, curr = 1;
for (int i = 2; i <= n; ++i) {
int next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}

int main() {
constexpr int fact7 = factorial(7);
constexpr int fib10 = fibonacci(10);

std::cout << "7! = " << fact7 << std::endl;
std::cout << "Fibonacci(10) = " << fib10 << std::endl;

return 0;
}

These enhancements are a game changer and made constexpr functions use in template meta-programing so elegant.
For example in C++11 if you want to write a function that can find the maximum in an array at compile time we have to implement that with complex recursion, ternary operator etc like this:

constexpr int max_helper(const int* arr, int size, int current_max, int index) {
return (index == size) ? current_max :
max_helper(arr, size,
(arr[index] > current_max ? arr[index] : current_max),
index + 1);
}

constexpr int find_max(const int* arr, int size) {
return max_helper(arr, size, arr[0], 1);
}

The same you can write much cleaner and more readable in C++14 like this with simple loop:

constexpr int find_max(const int* arr, int size) {
int max_val = arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > max_val) {
max_val = arr[i];
}
}
return max_val;
}

C++17: Compile-time branch selection using if constexpr:

C++17 introduced if constexpr to support compile time conditional branch selection. This is covered in detail in this post, as this beautiful topic deserves a separate post.

C++2O: Enhanced Compile-time Programming:

C++20 significantly expanded what can be done at compile time, bringing constexpr closer to being as powerful as regular runtime code.

Following are the enhancements added to constexpr in C++20:

  1. constexpr destructors
  2. constexpr dynamic memory allocation (new/delete)
  3. constexpr virtual functions
  4. constexpr try-catch blocks
  5. constexpr standard library containers
  6. constexpr algorithms

Its great but equally confusing !! So let me try to explain each of these in as simple words as possible.

1. constexpr destructors:

As you know in C++11, to make an object of a user defined class one of the requirement is the destructor of the class has to be trivial. i.e User should not declare a destructor.(the default destructor will be implicitly constexpr by compiler). But this has put the limitation for the programmer what if my program has special cleanup logic that needs to be added in the destructor ?

Get Sagar’s stories in your inbox

Join Medium for free to get updates from this writer.

Addition of the support for constexpr destructor enabled user-defined types (UDTs) with specific cleanup requirements to participate in constexpr contexts, supporting the creation of other constexpr features like containers.

#include <iostream>

struct ConstexprResource {
constexpr ConstexprResource() {}
constexpr ~ConstexprResource() {
// Cleanup operations that must run at compile time
}
};

constexpr void manage_resource() {
ConstexprResource r; // Constructor and destructor called at compile time
}

int main() {
constexpr auto result = manage_resource();
return 0;
}

2. constexpr dynamic memory allocation(new/delete):

C++20 added the support for using dynamic memory allocation inside a constexpr function provided the memory must be released in the same constexpr function context and the allocated memory should not persist to the runtime beyond the compilation.(Transient allocation)

A transient allocation is temporary dynamic memory created during constant evaluation that:

  • Exists only while the constexpr expression is being evaluated
  • Is never part of the final program’s runtime memory
  • Is automatically destroyed when constant evaluation finishes

It is heap-like memory that lives only in the compiler.

#include <iostream>

constexpr int sum_array_elements() {
int* arr = new int[4]{1, 2, 3, 4}; // Allocate at compile time
int sum = 0;
for (int i = 0; i < 4; ++i) {
sum += arr[i];
}
delete[] arr; // Deallocate at compile time
return sum;
}

int main() {
constexpr int result = sum_array_elements();
static_assert(result == 10);
std::cout << "Sum: " << result << std::endl;
return 0;
}

output:

Sum: 10

Note:
Vital for making standard library containers (std::vector, std::string) fully constexpr, allowing complex data structures to be built and processed entirely at compile time.

3. constexpr Virtual Functions:

C++20’s constexpr virtual functions enable compile-time polymorphism, which is particularly valuable for hardware abstraction layers.

Before C++20 You had to choose between:

  • Runtime polymorphism (virtual functions) — flexible but has runtime overhead
  • Compile-time techniques (templates/CRTP) — zero overhead but more complex syntax

Have a look at the below example how clean and elegant it is after this support:

#include <iostream>

struct MemoryChip {
constexpr virtual unsigned int capacity() const { return 0; }
constexpr virtual unsigned int pageSize() const { return 0; }
virtual ~MemoryChip() = default;
};

struct EEPROM_25LC160C : MemoryChip {
constexpr unsigned int capacity() const override { return 2048; }
constexpr unsigned int pageSize() const override { return 16; }
};

struct SRAM_23LC1024 : MemoryChip {
constexpr unsigned int capacity() const override { return 131072; }
constexpr unsigned int pageSize() const override { return 32; }
};

// COMPILE-TIME polymorphism - IMPOSSIBLE before C++20!
template<typename ChipType>
constexpr unsigned int getPages() {
ChipType chip;
MemoryChip* ptr = &chip; // Polymorphic pointer
return ptr->capacity() / ptr->pageSize(); // Virtual dispatch at compile time!
}

int main() {
constexpr unsigned int eeprom_pages = getPages<EEPROM_25LC160C>();
constexpr unsigned int sram_pages = getPages<SRAM_23LC1024>();

static_assert(eeprom_pages == 128); // 2048 / 16
static_assert(sram_pages == 4096); // 131072 / 32

std::cout << "EEPROM Pages: " << eeprom_pages << std::endl;
std::cout << "SRAM Pages: " << sram_pages << std::endl;

return 0;
}

output:

EEPROM Pages: 128
SRAM Pages: 4096

Different hardware chips, same polymorphic interface, all resolved at compile time through virtual function calls!

Note:
Virtual dispatch happens at COMPILE TIME in constexpr contexts!

4. constexpr try-catch blocks:

Allows library writers to maintain exception safety guarantees while still permitting their code to be used in constexpr contexts.

#include <iostream>
#include <stdexcept>

constexpr int safe_divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero!");
}
return a / b;
}

constexpr int compute_quotient(int x) {
try {
return safe_divide(100, x);
} catch (const std::runtime_error&) {
return -1;
}
}

int main() {
constexpr int result1 = compute_quotient(25);
constexpr int result2 = compute_quotient(0);

static_assert(result1 == 4);
static_assert(result2 == -1);

std::cout << "100 / 25 = " << result1 << std::endl;
std::cout << "100 / 0 = " << result2 << " (error handled)" << std::endl;

return 0;
}

output:

100 / 25 = 4
100 / 0 = -1 (error handled)

5. constexpr standard library containers:

C++20 allows the use of dynamic containers inside constexpr functions .
Enables the preparation of complex, pre-processed data structures entirely at compile time, eliminating runtime initialization overhead.

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

constexpr auto get_sorted_vector_back() {
std::vector<int> my_vec = {1, 4, 2, 3}; // Works at compile time
std::sort(my_vec.begin(), my_vec.end()); // Works at compile time
return my_vec.back();
}

constexpr std::vector<int> create_squares(int n) {
std::vector<int> squares;
for (int i = 1; i <= n; ++i) {
squares.push_back(i * i);
}
return squares;
}

int main() {
constexpr int max_val = get_sorted_vector_back();
static_assert(max_val == 4);

std::cout << "Max value: " << max_val << std::endl;

return 0;
}

output:

Max value: 4

Note:
Its only allowed inside a constexpr function .
You can write something like this: constexpr std::vector<int> data{1,2,3} outside of the constexpr function. So don’t get confused.
The most important point to remember is you can use containers like std::vector etc, or std::string inside constexpr functions only. Since its in the context of constexpr functionso will be evaluated during compile time.

6. contexpr algorithms:

100+ standard algorithms are now constexpr in C++20.
This Enabled complex compile-time computations.
For example:

#include <iostream>
#include <algorithm>
#include <array>

constexpr std::array<int, 4> get_sorted_array() {
std::array<int, 4> arr = {3, 1, 4, 2};
std::sort(arr.begin(), arr.end()); // std::sort is constexpr in C++20
return arr;
}

constexpr int find_max_with_algorithm() {
std::array<int, 10> arr = {5, 2, 8, 1, 9, 3, 7, 4, 6, 10};

// Use std::max_element at compile time!
auto max_it = std::max_element(arr.begin(), arr.end());
return *max_it;
}

int main() {
constexpr auto sorted_arr = get_sorted_array();
constexpr int max_val = find_max_with_algorithm();

static_assert(sorted_arr[0] == 1 && sorted_arr[3] == 4);
static_assert(max_val == 10);

std::cout << "Sorted array: ";
for (int val : sorted_arr) {
std::cout << val << " ";
}
std::cout << std::endl;

std::cout << "Max value: " << max_val << std::endl;

return 0;
}

output:

Sorted array: 1 2 3 4
Max value: 10

This support now permits utility functions that rely on common algorithms (sorting, searching, transforming data) to be evaluated at compile time to produce final, optimized results embedded directly into the executable.

From its humble beginnings in C++11 to its full power in C++20, constexpr has evolved into a true compile-time execution engine. By turning the compiler into an execution engine, constexpr has elevated C++ meta-programming from template tricks to real compile-time computation. A game changing feature indeed.

Thank you for reading !! Your feedback and thoughts are always welcome in the comments !!!

Found this article helpful? Please Clap 👏 and follow for more C++ and system programming content.

If you’re serious about passing technical interviews, try PracHub. It helped me structure my preparation with advanced mock sessions. [Check it out here] and start practicing. (Disclosure: This is an affiliate link).

--

--