CS100 Lecture 28
Compile-time Computations and Metaprogramming
Contents
- Example: Binary literals
constexpr
andconsteval
concept
and constraints- Summary
Example: Binary literals
Binary literals
Built-in binary literals support: C++14 and C23.
switch (rv32inst.opcode) { // 32-bit RISC-V instruction opcode
case 0b0110011: /* R-format */
case 0b0010011: /* I-format (not load) */
case 0b0000011: /* I-format: load */
case 0b0100011: /* S-format */
// ...
}
How do people write binary literals when there is no built-in support?
Runtime solution? No!
This is not satisfactory: The value is computed at run-time!
int dec2bin(int x) {
int result = 0, pow_two = 1;
while (x > 0) {
result += (x % 10) * pow_two;
x /= 10;
pow_two *= 2;
}
return result;
}
void foo(const RV32Inst &inst) {
const int forty_two = dec2bin(101010); // correct, but slow.
switch (inst.opcode) {
case dec2bin(110011): // Error! 'case' label must be compile-time constant!
// ...
}
}
Preprocessor metaprogramming solution
#
and ##
operators: Both are used in function-like macros.
#x
: stringify x
.
#define SHOW_VALUE(x) std::cout << #x << " == " << x
int ival = 42;
SHOW_VALUE(ival); // std::cout << "ival" << " == " << ival;
a##b
: Concatenate a
and b
.
#define DECLARE_HANDLER(name) void handler_##name(int err_code)
DECLARE_HANDLER(overflow); // void handler_overflow(int err_code);
Preprocessor metaprogramming solution
https://stackoverflow.com/a/68931730/8395081
#define BX_0000 0
#define BX_0001 1
#define BX_0010 2
// ......
#define BX_1110 E
#define BX_1111 F
#define BIN_A(x) BX_##x
#define BIN_B(x, y) 0x##x##y
#define BIN_C(x, y) BIN_B(x, y)
#define BIN(x, y) BIN_C(BIN_A(x), BIN_A(y))
const int x = BIN(0010, 1010); // 0x##BX_0010##BX_1010 ==> 0x2a ==> 42
Template metaprogramming solution
template <unsigned N> struct Binary {
static const unsigned value = Binary<N / 10>::value * 2 + (N % 10);
};
template <> struct Binary<0u> {
static const unsigned value = 0;
};
void foo(const RV32Inst &inst) {
const auto x = Binary<101010>::value; // 42
switch (inst.opcode) {
case Binary<110011>::value: // OK.
// ...
}
}
Compared to preprocessor metaprogramming, template metaprogramming is more powerful, and less error-prone.
Modern C++: constexpr
function
Just mark the function constexpr
, and the compiler will be able to execute it!
constexpr int dec2bin(int x) {
int result = 0, pow_two = 1;
while (x > 0) {
result += (x % 10) * pow_two; x /= 10; pow_two *= 2;
}
return result;
}
void foo(const RV32Inst &inst) {
switch (inst.opcode) {
case dec2bin(101010): // OK. Since 101010 is a compile-time constant,
// the function is executed at compile-time and
// produces a compile-time constant.
// ...
}
}
Metaprogramming
Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data.
- Read, generate, analyze or transform other programs, and even itself.
Typical problems that needs metaprogramming
Write a function that selects different behaviors at compile-time according to the argument?
- e.g. The
std::distance
function (in this week's recitation).
Generate some code according to the members of my class, without too much manual modification?
- e.g. Serialization: Generate
operator>>
automatically for my class that prints the members one-by-one? - e.g. "Metaclasses": Generate the getters and setters automatically for each of my data members?
......
Compile-time computations
How much work can be done in compile-time?
- Call to numeric functions with compile-time known arguments?
- e.g. Can
std::acos(-1)
be computed in compile-time? - Manipulation of compile-time known strings?
- e.g. Preparation of compile-time known regular expressions: CTRE
- Even crazier: Compile-time raytracer?!
- The computations are done entirely in compile-time. At run-time, the only work is to output the image.
Anything can be computed in compile-time, provided that the arguments are compile-time known!
constexpr
and consteval
constexpr
Constant expressions: expressions that are evaluated at compile-time.
constexpr
variables:
constexpr double dval = 5.2;
const int ival = 42;
By declaring a variable constexpr
, we mean that its value is compile-time known, and will not change.
- A
constexpr
variable is implicitlyconst
. - It must be initialized from a constant expression. Otherwise, an compile-error.
A const
variable initialized from a constant expression is also a constant expression.
constexpr
functions
constexpr
functions are potentially executed at compile-time:
- When the arguments are constant expressions, it is run at compile-time and produces a constant expression.
- When the arguments are not constant expressions, it is run at run-time just like a normal function.
constexpr int gcd(int a, int b) {
while (b != 0) { a = std::exchange(b, a % b); }
return a;
}
int main() {
const int x = 10, y = 16;
constexpr auto result = gcd(x, y); // OK. The result is a constant expression.
int n, m; std::cin >> n >> m;
std::cout << gcd(n, m) << '\n'; // OK. It is computed at run-time.
}
constexpr
member functions
Member functions may also be constexpr
. This is particularly useful for some very simple classes:
class StringView { // The 'StringView' class in lecture 27.
const char *mStart{nullptr};
std::size_t mLength{0};
public:
// constructors
constexpr StringView(const char *cstr);
constexpr StringView(const std::string &str);
// length
constexpr std::size_t size() const { return mLength; }
constexpr bool empty() const { return mStart; }
// searching
constexpr std::size_t find(char c, std::size_t pos = 0) const;
// ...
};
Evolution of constexpr
functions
constexpr
was first introduced in C++11, with many restrictions:
- A single
return
statement only. No loops or branches. constexpr
member functions are implicitlyconst
: They cannot modify the data members.virtual
functions cannot beconstexpr
.- Very little standard library support.
- ......
At that time, it was almost Turing-complete.
Evolution of constexpr
functions
In C++14:
- Multiple statements, loops and branches are allowed.
constexpr
member functions are no longer implicitlyconst
.constexpr
lambdas are still not yet allowed.- Definitely Turing-complete.
In C++17:
- Much more standard library support: A lot more functions are made
constexpr
since C++17. - Lambdas are automatically
constexpr
when it can be.
Evolution of constexpr
functions
C++20: A huge step!
constexpr
functions can perform dynamic memory allocations!- Memory allocated at compile-time must also be released at compile-time.
- Destructors can be
constexpr
! - Standard library containers like
std::vector
,std::string
can beconstexpr
! - Standard library algorithms are
constexpr
! virtual
functions can beconstexpr
!
C++20: constexpr
support in the standard library
#include <vector>
#include <algorithm>
constexpr int find_or_42(const std::vector<int> &vec, int target) {
auto found = std::ranges::find(vec, target); // compile-time search
return found == vec.end() ? 42 : *found;
}
int main() {
// 'vec' is initialized in compile-time!
constexpr auto result_1 = find_or_42({1, 4, 2, 8, 5, 7}, 10); // 42
constexpr auto result_2 = find_or_42({2, 3, 5, 7}, 3); // 3
static_assert(result_1 == 42);
static_assert(result_2 == 3);
}
constexpr
numeric functions
Since C++23, some simple numeric functions in <cmath>
, like abs
, ceil
, floor
, trunc
, round
, ... are constexpr
.
Since C++26, the power, square/cubic root, trigonometric, hyperbolic, exponential and logarithmic functions are all constexpr
!
constexpr
functions are pure
Pure functions (mathematical functions):
- Produce the same result when given the same arguments.
- Have no side effects. They cannot modify the value of variables outside them.
- Don't change the state of the program.
consteval
: Immediate functions
consteval
generates an immediate function.
- Every call of an immediate function generates a constant expression that is executed at compile-time.
consteval
- cannot be applied to destructors.
- has the same requirements as a
constexpr
function.
consteval
: Immediate functions
constexpr
: potentially executed at compile-time.consteval
: must be executed at compile-time.
consteval int sqr(int n) { return n * n; }
constexpr int r = sqr(100); // OK.
int x = 100; // 'x' is not a constant expression!
int r2 = sqr(x); // Error: 'sqr' must be called with constant expressions.
Note: A non-const
variable is not treated as a constant expression, even if initialized from a constant expression.
concept
and constraints
Motivating example: sorting a std::list
.
How should we sort a std::list
?
std::list<int> l = some_list();
std::sort(l.begin(), l.end()); // Is this correct?
Motivating example: sorting a std::list
.
How should we sort a std::list
?
std::list<int> l = some_list();
std::sort(l.begin(), l.end()); // No! We should use 'l.sort()'.
Ooops! std::sort
requires a pair of RandomAccessIterators, but std::list<T>::iterator
is not.
- But how can I understand the output of the compiler? It is complaining about "Invalid operands to
operator-
" ......
Motivating example: sorting a std::list
.
Let's look at how std::sort
is declared:
template <typename Iterator, typename Pred>
void sort(Iterator begin, Iterator end, Pred compare);
Iterator
is just a type deduced from the argument without any restrictions!
- In fact, this declaration accepts anything we pass to it.
- Errors can only be reported inside the function body, which might be in the form of "no match for ......".
Constraining the template arguments
template <typename Iterator, typename Pred>
void sort(Iterator begin, Iterator end, Pred compare);
Can we declare the function with some constraints on the type Iterator
?
- We need something like
cpp
template <RandomAccessIterator Iterator, typename Pred>
void sort(Iterator begin, Iterator end, Pred compare);
concept
A concept
is a named set of requirements often used to constrain the template arguments.
Examples:
template <typename T>
concept movable = std::is_object_v<T> &&
std::move_constructible<T> &&
std::assignable_from<T &, T> &&
std::swappable<T>;
template <typename T>
concept Incrementable = requires (T x) {
{ x++ } -> std::same_as<T>;
}
Example: Constrained algorithms
The C++20 algorithms in std::ranges
have well-defined constraints on the arguments:
template <std::random_access_iterator I, std::sentinel_for<I> S,
typename Comp = ranges::less, class Proj = std::identity>
requires std::sortable<I, Comp, Proj>
constexpr I sort(I first, S last, Comp comp = {}, Proj proj = {});
template<ranges::random_access_range R, typename Comp = ranges::less,
class Proj = std::identity>
requires std::sortable<ranges::iterator_t<R>, Comp, Proj>
constexpr ranges::borrowed_iterator_t<R>
sort(R&& r, Comp comp = {}, Proj proj = {});
Example: Constrained algorithms
This time, the compiler will produce some human-readable output on violation of the requirements:
std::list<int> l = some_list();
std::ranges::sort(l);
Example: Compile-time polymorphism
struct Shape {
virtual void draw() const = 0;
virtual ~Shape() = default;
};
struct Rectangle : Shape {
void draw() const override;
};
struct Circle : Shape {
void draw() const override;
};
void drawStuff(const Shape &s) {
s.draw();
}
template <typename T>
concept Shape = requires(const T x) {
x.draw();
};
struct Rectangle {
void draw() const; // non-virtual
};
struct Circle {
void draw() const; // non-virtual
};
template <Shape T>
void drawStuff(const T &s) {
s.draw();
}
How concept
and requires
benefit template code
Without the C++20 concept
and requires
, we need to write the requirements in a "hacky" way through SFINAE (Substitution Failure Is Not An Error):
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void increment(T &x) { ++x; }
With C++20:
template <std::integral T>
void increment(T &x) { ++x; }
Or even simpler:
void increment(std::integral auto &x) { ++x; }
Summary
The past
Back to 1979, the Bell Labs: C with Classes made by Bjarne Stroustrup.
- An object-oriented C with the ideas of "class" from Simula (and several other languages).
- Member functions, derived classes, constructors and destructors, protection mechanisms (
public
,private
,friend
), copy control throughoperator=
, ... - Better syntax, better type checking, ...
The past
After C with Classes was seen as a "medium success" by Stroustrup, he moved on to make a better new language - C++ was born (1983).
- Virtual functions, overloading, references,
const
, type checking, ... - Templates, exceptions, RTTI, namespaces, STL were added in the 1990s.
By the year 1998, C++ had become matured and standardized with the four major parts (Effective C++ Item 1):
- C
- Object-Oriented C++
- Template C++
- The STL
Entering Modern C++
A huge step since 2011:
- Rvalue references, move semantics, variadic templates, perfect forwarding
- Better template metaprogramming support
- Smart pointers
auto
anddecltype
: More benefit from the static type system- Lambdas,
std::function
andstd::bind
: Functional support - The concurrency library (
std::thread
,std::mutex
,std::atomic
, ...) constexpr
: Support for more straightforward compile-time computations.- ......
Evolution since C++11
More specialized library facilities:
optional
,any
,variant
,tuple
filesystem
: Standardized filesystem libraryregex
: The regular expression librarystring_view
: Heading towards the C++20views
andranges
More compile-time computation support:
- More restrictions on
constexpr
functions andauto
deduction are removed. - Class Template Argument Deduction (CTAD)
C++20 is historic!
CppCon2021 Talk by Bjarne Stroustrup: C++20: Reaching the aims of C++
C++20 is the first C++ standard that delivers on virtually all the features that Bjarne Stroustrup dreamed of in The Design and Evolution of C++ in 1994.
- Coroutines (Talk)
- Concepts and requirements (
concept
,requires
) (Talk) - Modules (Talk) (Talk on the implementation by MSVC)
- Ranges library
- Formatting library
- Three-way comparison (
operator<=>
,std::partial_ordering
, ...)
Future
- Static reflection and metaprogramming (Talk) (P2237R0) (P1240R1) (P2320R0)
- Metaclasses: Generative C++ (P0707R3)
- Pattern matching (Talk) (Herb Sutter's
cppfront
project) - Structured concurrency support (executors) (Talk) (P2300R6)
- Internal representation of C++ code suitable for analysis (Talk) (GitHub Page)
- ......