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:
Feature | constexpr | const |
---|---|---|
Compile-time Evaluation | Always evaluated at compile-time if possible. | Can be evaluated at either compile-time or runtime. |
Function Usage | Can be applied to functions to ensure compile-time evaluation. | Cannot be used for function definitions. |
Initialization | Must be initialized with a compile-time constant expression. | Can be initialized with runtime values. |
Type of Value | Requires a constant expression (known at compile-time). | Can be initialized with runtime values, but is immutable. |
Optimization | Ensures that the value is known and optimized at compile-time. | Allows for immutability but does not enforce compile-time optimization. |