Skip to content

constexpr functions

Video: Introduction to constexpr | Modern Cpp Series Ep. 86

C++11 introduced constexpr, to allow computations to be performed at compile time rather than runtime. This can lead to significant performance improvements since the result of constexpr computations is known at compile time and can be optimized by the compiler.

Basic constexpr Functions

A constexpr function is a function that can be evaluated at compile time. The compiler will compute the result during compilation and replace the function call with the computed value, which can lead to significant performance improvements.

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

int main() {
    constexpr int result = factorial(5);  // Computed at compile time
    std::cout << result << std::endl;     // 120

    // The compiler essentially replaces the function call with the literal 120
    // This is equivalent to writing: constexpr int result = 120;
}

Key points about constexpr functions:

  • They can be called at both compile time and runtime
  • When called in a constexpr context (like initializing a constexpr variable), they are evaluated at compile time
  • When called in a non-constexpr context, they behave like regular functions
  • The compiler can optimize them more aggressively

Compile-time vs Runtime

Understanding the difference between compile-time and runtime computation is crucial for effective use of constexpr. Let's examine how the same function behaves differently in different contexts:

cpp
constexpr int compile_time_factorial(int n) {
    return (n <= 1) ? 1 : n * compile_time_factorial(n - 1);
}

int runtime_factorial(int n) {
    return (n <= 1) ? 1 : n * runtime_factorial(n - 1);
}

int main() {
    // COMPILE TIME: This is computed during compilation
    constexpr int ct_result = compile_time_factorial(5);
    // The compiler calculates 5! = 120 and replaces ct_result with the literal 120

    // RUNTIME: This is computed when the program runs
    int rt_result = runtime_factorial(5);
    // The function call happens during program execution

    // COMPILE TIME: Even though it's a constexpr function, this is computed at compile time
    constexpr int ct_result2 = compile_time_factorial(10);

    // RUNTIME: This is computed at runtime because n is not a compile-time constant
    int n = 10;
    int rt_result2 = compile_time_factorial(n);
    // Even though it's a constexpr function, the result isn't known until runtime
}

Important distinction:

  • Compile-time computation: Happens during compilation, result is embedded in the executable
  • Runtime computation: Happens when the program runs, uses CPU cycles during execution
  • constexpr functions: Can be used in both contexts, but only compute at compile time when called in constexpr contexts

constexpr Variables

constexpr variables are compile-time constants that can be used in contexts requiring constant expressions. They provide better type safety and optimization opportunities compared to traditional #define macros.

cpp
constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159;
constexpr int ARRAY_SIZE = factorial(5);  // 120

int arr[ARRAY_SIZE];  // Array size known at compile time

// These can be used in template parameters
template<int N>
class Array {
    int data[N];
};

Array<MAX_SIZE> my_array;  // Compile-time size

Advantages over #define:

  • Type safety: constexpr variables have proper types
  • Scope: They respect C++ scoping rules
  • Debugging: They appear in debuggers
  • Template compatibility: Can be used in template parameters
  • constexpr functions: Can be computed using constexpr functions

Benefits of constexpr

The constexpr keyword provides several significant advantages over traditional runtime computation:

  1. Performance: Computations done at compile time eliminate runtime overhead
  2. Optimization: Compiler can optimize better when values are known at compile time
  3. Template metaprogramming: Much simpler than old template metaprogramming techniques
  4. Type safety: Compile-time type checking catches errors early
  5. Debugging: Compile-time errors are easier to debug than runtime errors
  6. Undefined behavior prevention: Compile-time evaluation catches UB early
  7. Memory efficiency: No runtime stack usage for constexpr computations
  8. Deterministic performance: Compile-time computations have predictable timing

Undefined Behavior Prevention

One of the most important benefits of constexpr is that it helps catch undefined behavior at compile time. Since constexpr functions must be evaluable at compile time, the compiler can detect many issues that would otherwise cause runtime undefined behavior:

cpp
// This would cause undefined behavior at runtime
int bad_division(int x, int y) {
    return x / y;  // Division by zero is UB
}

// This is safe - compiler will catch division by zero
constexpr int safe_division(int x, int y) {
    return x / y;  // Compile error if y is 0 in constexpr context
}

int main() {
    // This would compile but cause runtime UB
    int result1 = bad_division(10, 0);

    // This won't compile - compiler catches the error
    constexpr int result2 = safe_division(10, 0);  // Compile error!
}

Key advantages:

  • Early error detection: UB is caught during compilation, not at runtime
  • Better debugging: Compile-time errors are easier to track down
  • Safer code: Forces you to handle edge cases properly
  • Documentation: constexpr functions clearly indicate their requirements

Limitations and Considerations

While constexpr is powerful, it has some important limitations to be aware of:

  • Function complexity: Functions must be simple enough for compile-time evaluation
  • Recursion depth: Compiler-dependent limits on recursion depth
  • Operation restrictions: Not all operations are allowed in constexpr context (e.g., I/O, dynamic allocation)
  • Compile time: Complex constexpr computations can slow down compilation
  • Debugging complexity: Compile-time errors can be harder to debug than runtime errors
  • Compiler support: Different compilers may have different constexpr capabilities
  • Memory constraints: Very large constexpr computations may hit compiler memory limits

Best practices:

  • Start simple and gradually increase complexity
  • Test constexpr functions with both compile-time and runtime calls
  • Be aware of compiler-specific limitations
  • Use constexpr for performance-critical computations that can be done at compile time

Your Task

Implement a constexpr function for fibonacci calculation. Your function should:

  1. Be marked as constexpr
  2. Handle edge cases (0, 1)
  3. Use recursion for calculation
  4. Work in both compile-time and runtime contexts

The function should demonstrate the power of compile-time computation by allowing the compiler to pre-calculate values when possible.

Implement a constexpr function for compile-time fibonacci calculation.

cpp
constexpr int fibonacci(int n) {
    // TODO: Implement constexpr fibonacci function
    // - Handle edge cases (n <= 1)
}