Constexpr

Declaring variables using the const keyword introduces a distinction between compile-time constants and runtime constant in C++. The compiler implicitly determines whether a const variable is a compile-time or runtime constant based on its initializer. This distinction becomes crucial in scenarios where C++ demands a constant expression, as only compile-time constant variables can be used in such contexts.

When using const, our variables could end up as either a compile-time const or a runtime const, depending on whether the initializer is a constant expression or not. In some case, it can be hard to tell whether a const variable is a compile-time constant (and thus usable in a constant expression), or a runtime constant (and thus not usable in a constant expression).

For example:

int a { 5 };       // not const at all
const int b { x }; // obviously a runtime const (since initializer is non-const)
const int c { 5 }; // obviously a compile-time const (since initializer is a constant expression)

const int d { obj };        // not obvious whether this is a runtime or compile-time const
const int e { getValue() }; // not obvious whether this is a runtime or compile-time const

In the above example, both d and e could be either a runtime or a compile-time const depending on how obj and getValue() are defined. It's not clear until we hunt down the definitions for those identifiers.

The constexpr keyword

It stands for “constant expression”. It defines variables or functions that must be evaluated at compile-time, enabling the compiler to optimize code. The key difference between constexpr and const is that while const can be initialized with a runtime value, constexpr requires a compile-time constant expression.

The constexpr keyword in C++ serves as a powerful tool for ensuring that variable is a compile-time constant. By using constexpr instead of const in a variable's declaration, developers can explicitly communicate their intent to the compiler that the variable should be a constant expression. This enforcement comes with the requirement that the constexpr variable must be initialized with a constant expression, and any deviation from this rule leads to a compilation error.

constexpr is a keyword introduced in C++11 to define compile-time constants. It has since been enhanced with each new version of C++ (C++14, C++17, C++20), making it a powerful tool for optimizing code. When a variable or function is marked as constexpr, the compiler ensures that its value can be computed at compile-time.

Syntax:

constexpr type variable = value;

For example:

#include <iostream>

int five()
{
    return 5;
}

int main()
{
    constexpr double gravity { 9.8 }; // ok: 9.8 is a constant expression
    constexpr int sum { 4 + 5 };      // ok: 4 + 5 is a constant expression
    constexpr int something { sum };  // ok: sum is a constant expression

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;

    constexpr int myAge { age };      // compile error: age is not a constant expression
    constexpr int f { five() };       // compile error: return value of five() is not a constant expression

    return 0;
}

Key Characteristics of constexpr:

  • A constexpr variable must be initialized with a constant expression. This means the value assigned to it must be computable at compile time.
  • The constexpr keyword signals to the compiler that the variable's value can be computed at compile time, offering optimization opportunities.
  • The use of constexpr guarantees that the variable is indeed a compile-time constant, allowing it to be utilized in constant expressions.
  • When a function or variable is marked as constexpr, the compiler can evaluate it at compile time, potentially avoiding runtime computations.

Example Usage of constexpr:

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

int main() {
    constexpr int side = 5;
    constexpr int area = square(side);

    // Compile-time constants can be used in constant expressions
    int array[area];  // Valid since 'area' is a compile-time constant

    return 0;
}

constexpr Functions:

One of the most powerful features of constexpr is that it can be used with functions. A function declared as constexpr guarantees that it can be evaluated at compile-time if all its arguments are constant expressions. If the arguments are not constant expressions, the function can still be evaluated at runtime, making constexpr functions both compile-time and runtime capable.

Syntax:

constexpr returnType functionName(parameters) {
    // Function body
}

Example of constexpr Function:

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

int main() {
    constexpr int result = square(5);  // Compile-time evaluation
    int runtime_value = 10;
    int runtime_result = square(runtime_value);  // Runtime evaluation
}

In this example:

  • When square(5) is called with a constant argument, it is evaluated at compile-time.
  • When square(runtime_value) is called with a variable, it is evaluated at runtime.

Evolution of constexpr

C++11: The Birth of constexpr

In C++11, constexpr was introduced with strict requirements. constexpr functions could only contain a single return statement and couldn’t have branches, loops, or any other complex logic. These limitations made it somewhat restrictive for real-world usage.

Example (C++11):

constexpr int add(int x, int y) {
    return x + y;  // Simple function with a single return statement
}

C++14: Relaxed Rules for constexpr Functions

C++14 relaxed many of the restrictions imposed by C++11. With C++14, constexpr functions could now include multiple statements, local variables, loops, branches (if, else, switch), and more, allowing more complex logic to be evaluated at compile-time.

Example (C++14):

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

In this example, the factorial function uses a loop and local variable to compute the factorial of a number. If n is a constant expression, the function will be evaluated at compile-time.

C++17: constexpr with if-constexpr

C++17 introduced a new feature called if constexpr, which allows conditional compilation based on compile-time constants. This is especially useful in template metaprogramming, where different code paths are chosen based on template arguments known at compile-time.

Example (C++17):

template <typename T>
constexpr T absoluteValue(T x) {
    if constexpr (std::is_unsigned<T>::value) {
        return x;  // Unsigned types cannot be negative
    } else {
        return x < 0 ? -x : x;  // Signed types need the conditional check
    }
}

Here, if constexpr ensures that the check for negative values only applies to signed types, leading to more efficient code for unsigned types.

C++20: consteval and Expanded constexpr Features

C++20 introduced consteval, which is similar to constexpr but stricter. A consteval function must always be evaluated at compile-time, whereas a constexpr function can still be evaluated at runtime if necessary.

Additionally, C++20 further expanded the capabilities of constexpr functions to allow dynamic memory allocation (new and delete), try-catch blocks, and virtual functions.

Example of consteval (C++20):

consteval int getCompileTimeValue() {
    return 42;
}

int main() {
    int x = getCompileTimeValue();  // This function must always be evaluated at compile-time
}

const vs constexpr

Although both constexpr and const are used for constants, they serve different purposes:

Featureconstexprconst
Compile-time EvaluationAlways evaluated at compile-time if possible.Can be evaluated at either compile-time or runtime.
Function UsageCan be applied to functions to ensure compile-time evaluation.Cannot be used for function definitions.
InitializationMust be initialized with a compile-time constant expression.Can be initialized with runtime values.
Type of ValueRequires a constant expression (known at compile-time).Can be initialized with runtime values, but is immutable.
OptimizationEnsures that the value is known and optimized at compile-time.Allows for immutability but does not enforce compile-time optimization.