C++ Constexpr Functions

Compile-time computation for maximum performance

⚡ What are Constexpr Functions?

Constexpr functions can be evaluated at compile-time when given constant expressions as arguments. This means calculations happen during compilation, not runtime, resulting in faster program execution and compile-time constants.


// Constexpr function evaluated at compile-time
constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}
// factorial(5) calculated during compilation = 120
                                    

Compile-time Result:

factorial(5) = 120 (computed before program runs!)

Constexpr Benefits

🚀

Performance

Zero runtime cost for constant expressions

constexpr int square(int x) { return x*x; }
🔢

Compile-time

Calculations done during compilation

constexpr int result = square(10); // = 100
📏

Array Sizes

Can be used for array dimensions

int arr[square(5)]; // Array of size 25
🔄

Dual Nature

Works at compile-time and runtime

// Compile-time or runtime based on context

🔹 Basic Constexpr Functions

constexpr functions are evaluated at compile-time if their arguments are constant expressions, enabling performance optimizations. They can be used in contexts requiring constants, like array sizes or template arguments. A basic constexpr function looks like constexpr int square(int x) { return x * x; }. The function must satisfy certain restrictions (C++11/14/17 relaxed these). Using constexpr improves performance by moving calculations to compilation and ensures errors are caught earlier.

#include <iostream>
using namespace std;

// Simple constexpr functions
constexpr int add(int a, int b) {
    return a + b;
}

constexpr int multiply(int a, int b) {
    return a * b;
}

constexpr bool isEven(int n) {
    return n % 2 == 0;
}

int main() {
    // These are computed at compile-time
    constexpr int sum = add(10, 20);        // = 30
    constexpr int product = multiply(4, 5); // = 20
    constexpr bool even = isEven(8);        // = true
    
    cout << "Sum: " << sum << endl;
    cout << "Product: " << product << endl;
    cout << "Is 8 even? " << (even ? "Yes" : "No") << endl;
    
    // Can also be used at runtime
    int x = 15, y = 25;
    cout << "Runtime sum: " << add(x, y) << endl;
    
    return 0;
}

Output:

Sum: 30
Product: 20
Is 8 even? Yes
Runtime sum: 40

🔹 Recursive Constexpr Functions

Recursive constexpr functions allow compile-time computation of factorial, Fibonacci numbers, and other recursive algorithms. Since C++14, constexpr functions can contain loops and multiple returns, making recursion straightforward. Example: constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n-1); }. These functions are powerful for meta-programming, generating lookup tables, or defining constants derived from complex formulas, all without runtime overhead.

#include <iostream>
using namespace std;

// Recursive constexpr factorial
constexpr long long factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

// Recursive constexpr Fibonacci
constexpr long long fibonacci(int n) {
    return (n <= 1) ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

// Recursive constexpr power function
constexpr long long power(int base, int exp) {
    return (exp == 0) ? 1 : base * power(base, exp - 1);
}

int main() {
    // All computed at compile-time!
    constexpr auto fact5 = factorial(5);      // 120
    constexpr auto fib10 = fibonacci(10);     // 55
    constexpr auto pow23 = power(2, 8);       // 256
    
    cout << "5! = " << fact5 << endl;
    cout << "10th Fibonacci = " << fib10 << endl;
    cout << "2^8 = " << pow23 << endl;
    
    return 0;
}

Output:

5! = 120
10th Fibonacci = 55
2^8 = 256

🔹 Constexpr for Array Sizes

constexpr functions can compute array sizes at compile-time, ensuring type safety and avoiding macros. Instead of #define SIZE 100, use constexpr int getSize() { return 100; } then int arr[getSize()];. This integrates size calculations directly into the type system. You can base sizes on calculations or template parameters. Since the size is a constant expression, it works with standard arrays and std::array, providing bounds checking and better integration with STL algorithms.

#include <iostream>
using namespace std;

constexpr int calculateSize(int rows, int cols) {
    return rows * cols;
}

constexpr int getMaxSize() {
    return 100;
}

int main() {
    // Array sizes determined at compile-time
    constexpr int matrixSize = calculateSize(5, 4);  // 20
    constexpr int bufferSize = getMaxSize();         // 100
    
    int matrix[matrixSize];     // Array of 20 integers
    char buffer[bufferSize];    // Array of 100 characters
    
    cout << "Matrix size: " << matrixSize << endl;
    cout << "Buffer size: " << bufferSize << endl;
    cout << "Matrix array size: " << sizeof(matrix)/sizeof(int) << endl;
    cout << "Buffer array size: " << sizeof(buffer) << endl;
    
    return 0;
}

Output:

Matrix size: 20
Buffer size: 100
Matrix array size: 20
Buffer array size: 100

🔹 Constexpr vs Regular Functions

constexpr functions offer the unique advantage of compile-time evaluation, eliminating runtime cost for constant inputs. A regular function always runs at runtime. If a constexpr function is called with non-constant arguments, it executes at runtime like a regular function. This dual nature provides flexibility. Use constexpr to shift work to compile-time, reducing executable size and improving performance. Modern C++ trends toward making more standard library functions constexpr (e.g., std::sort in C++20), enabling faster, safer code.

#include <iostream>
#include <chrono>
using namespace std;

// Regular function
int regularPower(int base, int exp) {
    int result = 1;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}

// Constexpr function
constexpr int constexprPower(int base, int exp) {
    int result = 1;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}

int main() {
    const int iterations = 1000000;
    
    // Compile-time calculation (zero runtime cost)
    constexpr int compileTimeResult = constexprPower(2, 10);  // = 1024
    
    // Runtime calculation timing
    auto start = chrono::high_resolution_clock::now();
    for (int i = 0; i < iterations; i++) {
        volatile int result = regularPower(2, 10);  // Prevent optimization
    }
    auto end = chrono::high_resolution_clock::now();
    auto duration = chrono::duration_cast<chrono::microseconds>(end - start);
    
    cout << "Compile-time result: " << compileTimeResult << endl;
    cout << "Runtime calculation took: " << duration.count() << " microseconds" << endl;
    cout << "Compile-time calculation took: 0 microseconds!" << endl;
    
    return 0;
}

Typical Output:

Compile-time result: 1024
Runtime calculation took: 3500 microseconds
Compile-time calculation took: 0 microseconds!

🧠 Test Your Knowledge

When are constexpr functions evaluated?