C++ Constexpr
Introduction
In modern C++, constexpr
is one of the most powerful features introduced to enhance performance by enabling more computation to happen at compile time rather than runtime. First introduced in C++11 and significantly enhanced in C++14 and C++17, constexpr
allows the programmer to specify that the value of a variable or the result of a function can be computed at compile time.
This feature is particularly valuable because:
- Performance: Computations performed at compile time don't incur runtime costs
- Embedded Systems: It enables complex calculations for resource-constrained environments
- Template Metaprogramming: It simplifies many complex metaprogramming techniques
Let's dive into how constexpr
works and how you can use it in your C++ programs.
Basic Usage of constexpr
constexpr Variables
At its simplest, constexpr
can be used to declare variables that are guaranteed to be computed at compile time:
constexpr int answer = 42; // Simple constant
constexpr double pi = 3.14159265358979323; // Mathematical constant
constexpr int square_of_7 = 7 * 7; // Compile-time computation
While this looks similar to using const
, there's an important distinction: constexpr
ensures that the value is computed at compile time, while const
only promises that the variable won't be modified.
constexpr Functions
The real power of constexpr
comes when applied to functions:
constexpr int square(int x) {
return x * x;
}
// This will be computed at compile-time
constexpr int result = square(7); // result = 49, determined during compilation
// This can be computed at either compile-time or runtime
int input;
cin >> input;
int dynamic_result = square(input); // Computed at runtime
Let's see this in action with a full example:
#include <iostream>
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
int main() {
// Computed at compile-time
constexpr int fact5 = factorial(5);
std::cout << "5! = " << fact5 << std::endl;
// Can also be used at runtime
int n;
std::cout << "Enter a number to calculate factorial: ";
std::cin >> n;
// This call happens at runtime
std::cout << n << "! = " << factorial(n) << std::endl;
return 0;
}
Output:
5! = 120
Enter a number to calculate factorial: 6
6! = 720
Evolution of constexpr Across C++ Versions
The capabilities of constexpr
have expanded with each C++ version:
C++11: Initial Introduction
- Basic arithmetic operations
- Simple control flow (ternary operator only)
- No loops, only recursion
- Limited to a single
return
statement
// C++11 constexpr function - must be a single return statement
constexpr int max_cpp11(int a, int b) {
return a > b ? a : b;
}
C++14: Enhanced Capabilities
- Multiple statements allowed
- Local variables
- Loops (for, while)
- Multiple return paths
- Mutation of local variables
// C++14 constexpr function - can use more complex logic
constexpr int factorial_cpp14(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
C++17: Further Improvements
- if constexpr for compile-time conditional execution
- Lambda expressions can be constexpr
- More standard library functions became constexpr
// C++17 if constexpr example
template <typename T>
constexpr auto get_value(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t;
} else {
return t;
}
}
C++20: Even More Power
- Constexpr containers and algorithms
consteval
andconstinit
keywords- Virtual function calls
- try-catch blocks
- Dynamic memory allocation (new/delete)
Practical Applications
Example 1: Compile-time Lookup Tables
One common use for constexpr
is generating lookup tables at compile time:
#include <iostream>
#include <array>
// Create a compile-time sine table
constexpr double pi = 3.14159265358979323846;
constexpr double to_radians(double degrees) {
return degrees * (pi / 180.0);
}
constexpr double sine_impl(double x) {
// Simple Taylor series approximation for sin(x)
double result = 0;
double term = x;
double factorial = 1;
for (int i = 1; i <= 9; i += 2) {
result += (i % 4 == 1 ? 1.0 : -1.0) * term / factorial;
term *= x * x;
factorial *= (i + 1) * (i + 2);
}
return result;
}
constexpr std::array<double, 91> create_sine_table() {
std::array<double, 91> result{};
for (int i = 0; i <= 90; i++) {
result[i] = sine_impl(to_radians(i));
}
return result;
}
// Our lookup table is computed at compile time
constexpr auto sine_table = create_sine_table();
int main() {
// No runtime computation for these lookups
std::cout << "sin(0°) = " << sine_table[0] << std::endl;
std::cout << "sin(30°) = " << sine_table[30] << std::endl;
std::cout << "sin(45°) = " << sine_table[45] << std::endl;
std::cout << "sin(60°) = " << sine_table[60] << std::endl;
std::cout << "sin(90°) = " << sine_table[90] << std::endl;
return 0;
}
Output:
sin(0°) = 0
sin(30°) = 0.5
sin(45°) = 0.707107
sin(60°) = 0.866025
sin(90°) = 1
Example 2: Compile-time String Processing
With C++17 and if constexpr
, we can do powerful string operations at compile time:
#include <iostream>
#include <string_view>
// Compile-time function to count occurrences of a character
constexpr size_t count_char(std::string_view str, char c) {
size_t count = 0;
for (auto ch : str) {
if (ch == c) count++;
}
return count;
}
// Compile-time function to check if a string is a palindrome
constexpr bool is_palindrome(std::string_view str) {
for (size_t i = 0; i < str.size() / 2; i++) {
if (str[i] != str[str.size() - i - 1]) {
return false;
}
}
return true;
}
int main() {
// These are evaluated at compile time
constexpr auto a_count = count_char("banana", 'a');
constexpr auto is_pal1 = is_palindrome("racecar");
constexpr auto is_pal2 = is_palindrome("hello");
std::cout << "Number of 'a's in 'banana': " << a_count << std::endl;
std::cout << "'racecar' is a palindrome: " << (is_pal1 ? "true" : "false") << std::endl;
std::cout << "'hello' is a palindrome: " << (is_pal2 ? "true" : "false") << std::endl;
return 0;
}
Output:
Number of 'a's in 'banana': 3
'racecar' is a palindrome: true
'hello' is a palindrome: false
Example 3: Compile-time Type Traits
constexpr
can be used for compile-time type operations:
#include <iostream>
#include <type_traits>
// A compile-time function to get the size of any type in bits
template <typename T>
constexpr size_t get_size_in_bits() {
return sizeof(T) * 8;
}
// A compile-time function to check if a type can hold another type's values
template <typename T, typename U>
constexpr bool can_hold_type() {
if constexpr (std::is_integral_v<T> && std::is_integral_v<U>) {
if constexpr (std::is_signed_v<T> == std::is_signed_v<U>) {
// Same signedness, simple size comparison
return sizeof(T) >= sizeof(U);
} else if constexpr (std::is_signed_v<T> && !std::is_signed_v<U>) {
// T is signed, U is unsigned
return sizeof(T) > sizeof(U);
} else {
// T is unsigned, U is signed
return false; // Conservative - would need more complex check
}
}
return false;
}
int main() {
// All evaluated at compile time
constexpr auto int_bits = get_size_in_bits<int>();
constexpr auto char_bits = get_size_in_bits<char>();
constexpr auto double_bits = get_size_in_bits<double>();
constexpr auto int_can_hold_char = can_hold_type<int, char>();
constexpr auto char_can_hold_int = can_hold_type<char, int>();
std::cout << "Size of int: " << int_bits << " bits" << std::endl;
std::cout << "Size of char: " << char_bits << " bits" << std::endl;
std::cout << "Size of double: " << double_bits << " bits" << std::endl;
std::cout << "int can hold char: " << (int_can_hold_char ? "true" : "false") << std::endl;
std::cout << "char can hold int: " << (char_can_hold_int ? "true" : "false") << std::endl;
return 0;
}
Output:
Size of int: 32 bits
Size of char: 8 bits
Size of double: 64 bits
int can hold char: true
char can hold int: false
Best Practices and Limitations
When to Use constexpr
- For values that can be computed at compile time
- For functions that might be used in both compile-time and runtime contexts
- For creating compile-time lookup tables or data structures
- For optimizing performance-critical code
- For template metaprogramming
Limitations
-
C++11 Restrictions: In C++11,
constexpr
functions can only contain a single return statement. Later standards relaxed this requirement. -
Library Support: Not all standard library functions are marked as
constexpr
, though this is improving with each C++ version. -
Debugging: Compile-time errors in complex
constexpr
functions can be harder to debug. -
Compilation Time: Heavy use of
constexpr
can increase compilation times.
Best Practices
-
Keep it Simple: Make
constexpr
functions clear and straightforward. -
Use Where Beneficial: Don't mark everything as
constexpr
just because you can. -
Fallback to Runtime: Design
constexpr
functions to work correctly at runtime too.
// Example of good practice - works at both compile-time and runtime
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
int tmp = a + b;
a = b;
b = tmp;
}
return b;
}
Visual Representation: Compile-Time vs. Runtime Evaluation
Summary
constexpr
is a powerful feature in modern C++ that enables computation at compile time. Its benefits include:
- Better Performance: Calculations are done during compilation, eliminating runtime overhead.
- Code Safety: Errors in
constexpr
functions are caught at compile time. - Enhanced Readability: Intent is clearer compared to older metaprogramming techniques.
- Flexibility: The same function can be used for both compile-time and runtime computations.
As C++ evolves, constexpr
continues to gain capabilities, making it an increasingly important tool for C++ developers seeking to optimize their code.
Exercises
- Write a
constexpr
function to calculate the nth Fibonacci number. - Create a compile-time lookup table for the squares of numbers from 1 to 20.
- Write a
constexpr
function that checks if a given number is prime. - Implement a compile-time function to calculate the greatest common divisor (GCD) of two numbers.
- Create a
constexpr
function that reverses a string_view, and use it to check for palindromes at compile time.
Additional Resources
- C++ Reference: constexpr specifier
- CppCon 2015: Scott Schurr "constexpr: Applications"
- C++ Core Guidelines: Compile-time computation
- Book: "Effective Modern C++" by Scott Meyers, Item 15: Use constexpr whenever possible
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)