This is my learning notes from the course - Mastering C++ Standard Library Features.
Programming at Compile-Time
- Compile-time computations with
constexpr
- Manipulation of variadic template type lists
- Standard Library features focused on metaprogramming
Constant Expressions
- What constant expression means?
constexpr
in C++11/14/17- Practical use cases of
constexpr
functions
What Does Constant Expression Mean?
- You might have noticed that values as non-type template parameters or array sizes do not accept run-time values
1
2
3
4
5
6
7
8
9
10
11template <int I> struct foo { };
int main()
{
int i;
std::cin >> i;
foo<i>{}; // <-- compile-time error
return 0;
} - This makes sense, as these values must be known at compile-time
- The C++ Standard refers to expressions whose result can be computed at compile-time as “constant expressions“
1
2
3
4
5
6
7
8template <int I> struct foo { };
int main()
{
foo<5>{}; // <-- OK, `5` is a constant expression
return 0;
}
Constant Expressions - C++03
- Before C++11, only a few language elements were valid constant expressions:
- Expressions consisting of arithmetic literals and enumerators
- Non-
volatile const
variables orstatic
data members - Non-type
template
parameters sizeof
expressions
- C++11 and C++14 greatly relaxed these limitations:
https://en.cppreference.com/w/cpp/language/constant_expression
Constant Expressions - C++11
Since C++11, users can now write functions computing constant expressions thanks to the
constexpr
keyword1
2
3
4
5constexpr int five() { return 5; }
std::array<int, five()> foo;
// ^~~~~~
// constant expressionIn C++11,
constexpr
functions were limited to a singlereturn
statement - this limitation required developers to use recursion or unnatural code in order to produce useful results1
2
3
4
5
6constexpr int fibonacci(int x)
{
return x == 0 ? 0
: x == 1 ? 1
: fibonacci(x - 1) + fibonacci(x - 2);
}
constexpr
functions can be executed both at run-time and compile-time- If all the arguments are known at compile-time, the function might be evaluated at compile-time
- If any of the arguments is only known at run-time, the function cannot be evaluated at compile-time
- If the function is invoked in a context where a constant expression is required, compile-time evaluation is mandatory
constexpr
can also be applied to variables - it forces variables to be initialized with a constant expressions1
2constexpr int n0 = 42; // <-- OK
constexpr int n1; // <-- ERROR1
2int read_int_from_stdin();
constexpr int n2 = read_int_from_stdin(); // <-- ERROR1
2
3constexpr int triplicate(int);
constexpr int n3 = triplicate(5); // <-- OK
static_assert(n3 == 15); // <-- OK
Constant Expressions - C++14
Since C++14,
constexpr
functions restrictions were greatly relaxedAlmost every language feature can be used inside the body of a
constexpr
function, except:- Dynamic allocation
- Run-time based memory accesses and casts
- Expressions that would lead to undefined behavior
- Function calls to non-
constexpr
functions - (…)
An example of Fibonacci without recursion:
1
2
3
4
5
6
7
8
9
10
11
12
13constexpr int fibonacci(int x)
{
int a{0}, b{1};
for(int i = 0; i < x; i++)
{
const int temp = b;
b += a;
a = temp;
}
return a;
}
Constant Expressions - C++14
- Since C++17, lambdas can be
constexpr
as well1
[]{ return 10; } // implicitly `constexpr`
1
[]() constexpr { return 10; } // explicitly `constexpr`
1
2
3
4
5[]() constexpr // compile-time error
{
int i;
std::cin >> i;
}
Compile-Time Branching
- In C++17, we have compile-time branching:
if constexpr(...)
1
2
3
4
5
6
7
8
9
10
11
12template <typename T>
void foo(T x)
{
if constexpr(std::is_same_v<T, std::string>)
{
// ...
}
else
{
// ...
}
}
1 | if constexpr(std::is_same_v<T, std::string>) { /* ... */ } |
The condition must be a
constant expression
, otherwise will be compile-time errorBoth branches (true and false) need to be parsable
Only the taken branch will be instantiated
In the non-instantiated branch, code invalid for the current T is allowed
Example of regular
if-statement
:1
2
3
4
5
6
7
8
9template <typename T>
int foo(const T& x)
{
if(std::is_same_v<T, std::string>)
{
return x.length();
}
else { return x; }
}1
2foo(std::string{"abcd"}); // Compile-time error
foo(1234); // Compile-time errorExample of
if constexpr
:1
2
3
4
5
6
7
8
9template <typename T>
int foo(const T& x)
{
if constexpr(std::is_same_v<T, std::string>)
{
return x.length();
}
else { return x; }
}1
2foo(std::string{"abcd"}); // OK
foo(1234); // OK
Constant Expressions - The Future
- Proposals allowing dynamic memory allocation in
constexpr
functions are actively being discussed - A “
constexpr
allocator” was proposed to allow usage of STL containers inconstexpr
functions constexpr
functions might become as powerful as any other function
constexpr
- Why?
constexpr
functions allows us to produce constant expressions- If they’re invoked with run-time arguments, they will not produce constant expressions
- Constant expressions can be used for compile-time data structures and algorithms
- Performing computations at compile-time is safer and faster
- Compile-time errors instead of run-time errors
- Code won’t be generated or executed at run-time
Example: constexpr
Factorial in C++14
1 | constexpr int factorial(int n) |
1 | constexpr int f4 = factorial(4); |
- Using a
constexpr
variable requires its initialization expression to be a constant expression f4
is guaranteed to be evaluated at compile-time- We can therefore use
static_assert
1 | int f4 = factorial(4); |
f4
could be evaluated at compile-time, but it is not guaranteed- Note that a
constexpr
function can be evaluated at run-time
1 | int input; |
f_input
can only be evaluated at run-time- Note that a
constexpr
function can be evaluated at run-time
1 | constexpr int widget_count = 6; |
std::array
requires a constant expression as its second template parameter- Since
factorial
isconstexpr
, we can use it instead of hard-coding the result factorial
can be (and should be) unit tested
Example: Validating Strings at Compile-Time
1 | template <typename T> |
std::size
introduced in C++17 to homogeneously retrieve an array or a container’s size- Template - will work with both
std::string
andconst char*
, both run-time and compile-time
1 | static_assert(is_valid_user_id("U.ABCD") == true); |
- When a string known at compile-time is provided,
is_valid_user_id
will be evaluated as a constant expression
1 | std::string id; |
- The exact same function will work with values only known at run-time
The Tunnel is Deep
- Libraries such as
Sprout
orYuki
push the limits ofconstexpr
by providing complicated algorithms and data structures - Ben Deane and Jason Turner’s “
constexpr
ALL the Things!“ talk shows how to implement a compile-time JSON parser
Summary
- Learned that the
constexpr
functions can evaluate to constant expressions - Understood that
constexpr
functions might be evaluated at compile-time - Learned
constexpr
variables must be initialized with a constant expression - Studied that in C++14,
constexpr
functions do not have many limitations - Studied that in C++17, we have
constexpr
lambdas and compile-time branching withif constexpr(...)
Guidelines
- Always define your compile-time constants as
constexpr
:1
2
3constexpr int frame_rate = 60;
constexpr const char* debug_category = "app.rendering";
constexpr float precision = calculate(42.f); - Even in a local context,
constexpr
variables can increase readability:1
2
3
4
5auto read_file(const char* path)
{
constexpr int chunk_size{1024};
return read_chunks(chunk_size, path);
}
- Marked your functions as
constexpr
wherever possible - Considered
if constexpr(...)
when implementing static dispatch - Looked for opportunities to write and test
constexpr
functions instead of hardcoding pre-calculated computations - For example:- Constants
- Array sizes
- Loop conditions
- Validation