Written by:
Last updated: 11 July 2022
Please fill in the feedback form for lecture 8.
C++ metaprogramming allows you to generate code in the form of templates,
and to modify or augment the behaviour of your program, at compile time,
based on the types or values of arguments. Note that this is
not the same as reflection — we’ll get that in C++20
C++23 C++26 (hopefully) — which usually refers to
runtime manipulation.
In C++, metaprogramming entirely revolves around templates, but you should already be familiar with those (at least the basics) from the previous lectures.
One of the features you might have noticed being used without much
explanation is
constexpr. This essentially denotes compile-time stuff, but the meaning differs
slightly depending on where it’s used.
Note that
constexpr is
distinct from
const; while
constexpr
implies const, the converse is not true.
constexpr must
have values known at compile-time, while
const
variables can be initialised with runtime values — just that they can’t be
changed.
Before we talk about
constexpr
itself, we need to discuss what core constant expressions are.
Essentially, they are expressions that can be evaluated at compile-time.
They can be used as array sizes, non-type template arguments, etc. Note
that core constant expressions are not limited to
constexpr
settings. For example, the following is valid:
const int N = 16;
std::array<int, N> xs{};
constexpr but
core-constant-expression
Here, N was not
constexpr, but it was usable in a constant expression, because it fulfilled these
criteria:
constexpr
variable, OR
Strangely enough, core constant expressions are defined negatively, that is in terms of a blacklist of the types of things that can be evaluated in it. If none of those things are evaluated, then the expression qualifies as a core constant expression.
We won’t give the entire list (it’s too long), but some of the more important ones are:
constexpr
function (including constructors)
reinterpret_cast, including any conversion from
void*
to pointer-to-object
throw
expression
The key thing is that it only counts if the disqualifying expression
is evaluated — you can put these things in a
constexpr
function as long as control flow would not cause them to be evaluated.
Even so, there are a few more restrictions on
constexpr
functions themselves which we’ll discuss below.
constexpr variables
The first use of
constexpr is
on variables; this means that their value is available at compile time,
and can be used in constant expressions. It’s just another specifier, and
appears in much of the same places as one might see
const:
constexpr int x = 0;
struct Foo {
static constexpr int x = 10;
};
constexpr
variables
Note that
constexpr data
members (in structs) must be
static.
constexpr functions
The next use of constexpr is to mark functions, and it means
that you can use that function to compute values at compile-time to get a
constant expression. For example:
constexpr int square(int x) { //
return x * x;
}
constexpr
function
As we mentioned above, as long as an expression is not evaluated, there’s
isn’t a problem with it being non-constexpr. This means that the following function still qualifies as
constexpr:
constexpr int rectangle(int x, int y) {
if (x < 0 || y < 0) {
puts("your rectangle is wrong");
return 0;
} else {
return x * y;
}
}
constexpr
function
We can call this function at compile-time (and use it in situations where
we need a constant expression), as long as we don’t evaluate the
puts call (which is obviously not
constexpr):
constexpr int rekt = rectangle(69, 420);
static_assert(rekt == 28980);
constexpr
function
On the other hand, if we tried to call the function and control flow would
result in a call to a non-constexpr
function, then the result of the call is no longer a constant expression:
constexpr int rekt = rectangle(-1, -2);
$ make broken1.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken1.o broken1.cpp
broken1.cpp:18:19: error: constexpr variable 'rekt' must be initialized by a constant expression
constexpr int rekt = rectangle(-1, -2);
^ ~~~~~~~~~~~~~~~~~
broken1.cpp:8:5: note: non-constexpr function 'puts' cannot be used in a constant expression
puts("your rectangle is wrong");
^
broken1.cpp:18:26: note: in call to 'rectangle(-1, -2)'
constexpr int rekt = rectangle(-1, -2);
^
/usr/include/stdio.h:632:12: note: declared here
extern int puts (const char *__s);
^
1 error generated.
make: *** [Makefile:25: broken1.o] Error 1
constexpr
region results in the function call not being a constant expression
As we’ll see later though,
constexpr just
means that the function can be used to produce a constant
expression — not that it must produce one.
Finally, note that functions declared
constexpr or
consteval are
implicitly also
inline.
consteval functions
C++20 introduces the
consteval
specifier, which means that the evaluation of a function must happen at
compile time. This means that it cannot be called with any
runtime values.
For instance, if we made square
consteval
instead, then even at -O0, we don’t need any tricks to make
the compiler evaluate the function at compile-time:
consteval int square2(int x) { //
return x * x;
}
Note that
consteval is
stronger than
constexpr, so we don’t (and can’t) make a function both
constexpr and
consteval.
If we try to call the function with a (potentially) runtime value:
int k = 69;
int x = square2(k);
$ make broken2.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken2.o broken2.cpp
broken2.cpp:18:13: error: call to consteval function 'square2' is not a constant expression
int x = square2(k);
^
broken2.cpp:18:21: note: read of non-const variable 'k' is not allowed in a constant expression
int x = square2(k);
^
broken2.cpp:17:9: note: declared here
int k = 69;
^
1 error generated.
make: *** [Makefile:25: broken2.o] Error 1
consteval function with a non-constant
expression
There are other restrictions on
consteval
functions that go beyond
constexpr, like not being able to take pointers to them (unless you are also in a
consteval
context). Those rules are meant to prevent
consteval
functions from “escaping” to runtime.
constexpr functions
More formally,
constexpr
functions must satisfy the following requirements:
= default
or
= delete):
goto
statements
constexpr lambdas
You can also mark lambdas as
constexpr or
consteval
(covered below), like so:
[](auto a) constexpr {
/* ... */
};
This appears in the same place as
mutable. This isn’t usually required though, because lambdas are specified to be
implicitly
constexpr if
it satisfies all the requirements. The effect of this is to mark the
overloaded
operator()
as
constexpr or
consteval
respectively.
constexpr functions
One interesting use-case for
constexpr
functions is to perform arbitrary computation at compile-time. For
example, you can construct lookup tables in code instead of writing them
out by hand.
For example, let’s say we wanted to have a table of all the powers of 10 - 1 (so 9, 99, 999, etc.). We could have code like this:
constexpr std::array<int, 8> xs = {
9, 99, 999, 9999, 99999, 999999, 9999999, 99999999
};
or, we could be fancy and do something like this instead:
constexpr std::array<int, 8> xs = []() {
std::array<int, 8> ret{};
int foo = 10;
for (size_t i = 0; i < 8; i++) {
ret[i] = foo - 1;
foo *= 10;
}
return ret;
}();
constexpr functions to generate a lookup
table
Here, we used an immediately-invoked lambda expression to return the
result, since we need to initialise the the variable immediately with some
non-trivial code. This is fine because lambdas are implicitly
constexpr when
possible, and std::array is a literal type.
Of course this is a little contrived, but you can imagine scenarios with
more complicated lookup tables. For an interesting talk about how far you
can take
constexpr, you can watch
this talk
by Jason Turner.
Note that constant-initialised variables will end up in the
.data or .rodata section in the final
executable, so they’re pretty much the same as you just writing out the
data yourself.
As we saw above,
constexpr
functions and variables need to have a “literal type”, which is actually a
named requirement, like the iterator categories
(ForwardIterator, BidirectionalIterator, etc.) we saw
before.
A LiteralType is any of the following:
constexpr
destructor
constexpr
constructor, or is a compiler-generated lambda type
Note that for class types, its
constexpr
constructors don’t actually need to be accessible (in terms of
public/private) or even available (it can be
delete-ed) for it to qualify as a literal type.
One of the things that we need to take note of is that the compiler is not actually required to evaluate the function call at compile time! It is perfectly valid for it to actually just call the function at runtime.
For example, if we compile the following code and look at its assembly:
printf("5*5 = %d\n", //
square(5)); //
$ ./main.out | sed '1,/<1/d;/1>/,$d'
5*5 = 25
main: # @main
push rbp
mov rbp, rsp
mov edi, 5
call square(int) # here
mov esi, eax
movabs rdi, offset .L.str
mov al, 0
call printf
xor eax, eax
pop rbp
ret
constexpr
function
Here, we can see that the compiler still generated a call to the
square function, even though it could have
evaluated it at runtime and replaced it with the constant
25. Note this is with no optimisations (-O0); if
we used -O1 or above, then we get the expected behaviour —
but that would also be the case if the function was not
constexpr!
Of course, note that the compiler still needs to actually emit code for the function, and it might need to call it at runtime in legitimate cases — when the arguments are not known at compile time.
Either way, we can sort of get around this and force compile-time
evaluation by assigning to a
constexpr
variable first:
constexpr int x = square(5);
printf("5*5 = %d\n", x);
main: # @main
push rbp
mov rbp, rsp
sub rsp, 16
mov dword ptr [rbp - 4], 25
movabs rdi, offset .L.str
mov esi, 25 # here
mov al, 0
call printf
xor eax, eax
add rsp, 16
pop rbp
ret
constexpr
variable first lets the value be computed at compile-time
Note that this is done with no optimisations. However, this behaviour
isn’t actually guaranteed by the standard; it just so happens that
most compilers will compute assignments to
constexpr.
One way to actually force compile-time evaluation is to use the value as a non-type template parameter (which we will discuss next), because those must be constant expressions, and actually must be known at compile time:
template <auto X>
constexpr auto force_compile_time() {
return X;
}
constexpr int x = force_compile_time<square(4)>();
We also have the
consteval
specifier for functions as mentioned above, which enforces that all calls
to that function must produce a constant expression.
constinit specifier
One last specifier that we have is
constinit, which is applied to variables. When a variable is specified as such, it
is an assertion that the variable must be initialised with a constant
expression. However, unlike
constexpr, it does not require that the variable be
const-qualified.
That is, the following is valid:
static constinit int x //
= square(10);
x = 50;
std::cout << "x = " //
<< x << "\n";
$ ./main.out | sed '1,/<2/d;/2>/,$d'
x = 50
constinit
variables are not
const, so they can be modified
Note that local variables cannot be declared
constinit —
only variables with static or thread-local storage duration.
One of the main motivators for
constinit is
to avoid runtime initialisation of static variables, due to
static initialisation order fiasco. This way, we can be sure that the initialisation is run at compile-time
with a constant expression.
Lastly on the
constexpr
list, we have if-constexpr. This essentially allows us to check conditions
at compile-time, and we already saw some of this previously:
template <typename T>
constexpr bool is_arr() {
if constexpr (std::is_array_v<T>) {
return true;
} else {
return false;
}
}
std::cout << (is_arr<int[]>() //
? "true"
: "false")
<< "\n";
std::cout << (is_arr<int>() //
? "true"
: "false")
<< "\n";
$ ./main.out | sed '1,/<3/d;/3>/,$d'
true
false
Of course, this is quite a contrived example — we could’ve just used
std::is_array directly. The real power of constexpr-if is
that it discards the false clause of the if statement.
For example, if we had some generic code that accepted either a pointer or a reference and was supposed to return its value:
template <typename T>
auto get_value(const T& x) {
if (std::is_pointer_v<T>) {
return *x;
} else {
return x;
}
}
int x = 0;
int* y = &x;
get_value(x);
get_value(y);
$ make broken3.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken3.o broken3.cpp
broken3.cpp:9:12: error: indirection requires pointer operand ('int' invalid)
return *x;
^~
broken3.cpp:21:3: note: in instantiation of function template specialization 'get_value<int>' requested here
get_value(x);
^
broken3.cpp:11:5: error: 'auto' in return type deduced as 'int *' here but deduced as 'int' in earlier return statement
return x;
^
broken3.cpp:22:3: note: in instantiation of function template specialization 'get_value<int *>' requested here
get_value(y);
^
2 errors generated.
make: *** [Makefile:25: broken3.o] Error 1
It doesn’t work for either case! In the first case (where
T = int), it is as if we had the function on the
left, and in the second case (T = int*), the function on the
right:
int get_value(const int& x) {
if (false) {
return *x;
} else {
return x;
}
}
int get_value(const int*& x) {
if (true) {
return *x;
} else {
return x;
}
}
Even though the branches are either always-true or always-false, they
still need to typecheck — and since
int is not a
pointer, it cannot be dereferenced. In the second case, we got a complaint
about conflicting deduced return types — when the return type is
auto, all
return
statements must agree on the return type, which is not true here.
So, instead of making a function that works for both cases, we made one that works for neither!
If we used
constexpr
instead, like this:
template <typename T>
auto get_value(const T& x) {
if constexpr (std::is_pointer_v<T>) {
return *x;
} else {
return x;
}
}
int x = 69;
int* y = &x;
std::cout //
<< get_value(x) //
<< "\n";
std::cout //
<< get_value(y) //
<< "\n";
$ ./main.out | sed '1,/<4/d;/4>/,$d'
69
69
This works because in the first instantiation (T = int) of
the template, the condition is false, and it’s discarded; the compiler
never sees *x, so it works. In the second instantiation
(T = int*), the condition is true, so the else branch is
discarded; there’s now only one return statement, so there’s no conflict.
Note that for else if statements, you need
else if
constexpr (...).
constexpr-if
More formally, constexpr if statements are specified to discard the false branch of the if statement, and return statements in the discarded branch are not considered for function return-type deduction.
Furthermore, note that typechecking still applies when you use constexpr-if outside of a templated function; the false branch is still discarded, but since the types are all known, it must still typecheck. For example, the following will fail to compile:
void foo() {
if constexpr (false) {
int x = 10;
int y = *x;
}
}
Lastly, labels can only jump around within the same constexpr branch, and
you can’t use goto to jump into a if-constexpr branch.
static_assert
One of the things you might have seen us use before without much
explanation is
static_assert. It is essentially the compile-time equivalent of regular
assert, except that the boolean value must be a constant
expression (ie. known at compile-time).
You can also pass a message as the second argument, though it is optional. If the condition is false, then an error is issued at compile time, rather than at runtime.
For instance:
constexpr int x = 10 * 20;
static_assert(x < 69, "math");
$ make broken4.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken4.o broken4.cpp
broken4.cpp:6:3: error: static_assert failed due to requirement 'x < 69' "math"
static_assert(x < 69, "math");
^ ~~~~~~
1 error generated.
make: *** [Makefile:25: broken4.o] Error 1
static_assert
failure
If we tried to use a runtime value, it wouldn’t compile either, but with a different error message:
int x = 10 * 20;
static_assert(x < 69, "math");
$ make broken5.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken5.o broken5.cpp
broken5.cpp:6:17: error: static_assert expression is not an integral constant expression
static_assert(x < 69, "math");
^~~~~~
broken5.cpp:6:17: note: read of non-const variable 'x' is not allowed in a constant expression
broken5.cpp:5:7: note: declared here
int x = 10 * 20;
^
1 error generated.
make: *** [Makefile:25: broken5.o] Error 1
static_assert
cannot be used with runtime values
Note that the actual requirement is that the condition is a
constant expression — as discussed earlier, this includes
constant-initialised
const (but not
necessarily
constexpr) variables.
Another aspect of
static_assert
that is superior to regular asserts (other than being checked at
compile-time) is that they can appear at global (namespace) scope, and
in member scope, ie. inside the body of struct definitions. This
can let you assert certain invariants on the template parameters to your
class.
static_assert in templates
In templated code, you might want to assert that the type argument fulfils some criteria of course. Since we learned about constexpr-if just now, you might be tempted to write something like this:
template <typename T>
auto foozle(T x) {
if constexpr (std::is_pointer_v<T>) {
return *x;
} else {
static_assert(false, "no pointers!");
}
}
$ make broken6.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken6.o broken6.cpp
broken6.cpp:11:5: error: static_assert failed "no pointers!"
static_assert(false, "no pointers!");
^ ~~~~~
1 error generated.
make: *** [Makefile:25: broken6.o] Error 1
static_assert
Note that we didn’t even have to call the function — it just always fails! This is because the standard specifies that if no valid specialisation can be generated for a template, then the program is ill-formed1 — even if the template is not instantiated.
The way to get around this is to make the condition dependent — even if it ends up being always false. For example, this is a common idiom:
template <typename>
constexpr bool always_false = false;
template <typename T>
auto foozle(T x) {
if constexpr (std::is_pointer_v<T>) {
return *x;
} else {
static_assert(always_false<T>, "pointers only!");
}
}
int x = 10;
foozle(&x);
static_assert
a type-dependent expression
If we try to call it with a non-pointer, we get the expected failure:
int x = 0;
foozle(x);
$ make broken7.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken7.o broken7.cpp
broken7.cpp:13:5: error: static_assert failed due to requirement 'always_false<int>' "pointers only!"
static_assert(always_false<T>, "pointers only!");
^ ~~~~~~~~~~~~~~~
broken7.cpp:20:3: note: in instantiation of function template specialization 'foozle<int>' requested here
foozle(x);
^
1 error generated.
make: *** [Makefile:25: broken7.o] Error 1
static_assert
The first addition to templates that we’ll introduce is the ability to use
non-type parameters. In the templates we’ve seen so far, the template
parameters have always been types —
typename T or
class T.
C++ allows us to also pass values, and in fact we’ve already seen
this for std::array, which takes in both a type (the element
type) and a number (the size of the array).
To declare a non-type template parameter, simply use the type that you
want in place of
typename. For example, the template declaration of std::array might
look something like this:
template <typename ElemTy, size_t Length>
class array { /* ... */ };
std::array might be declared
And to use it, we simply do something like
std::array<int,
32>
— basically like a normal template with types.
If you were curious and opened all the boxes, we actually spoilered this in Lecture 1! By using a non-type parameter, we can get the compiler to tell us the size of an array argument:
template <size_t N>
void print_array(int (&xs)[N]) {
std::cout << "[ ";
for (size_t i = 0; i < N; i++) {
std::cout << (i ? ", " : "") //
<< xs[i];
}
std::cout << " ]\n";
}
int a1[] = {1, 2, 3};
std::cout << "a1 = ";
print_array(a1);
int a2[] = {1, 4, 9, 16, 25};
std::cout << "a2 = ";
print_array(a2);
$ ./main.out | sed '1,/<1/d;/1>/,$d'
a1 = [ 1, 2, 3 ]
a2 = [ 1, 4, 9, 16, 25 ]
If we look at the CppInsights link above, we see two separate
instantiations of the template —
print_array<3>
and
print_array<5>
— exactly what we would expect if we instantiated a type parameter with
different types.
Of course, we can take this further by templating the element type as well:
template <typename T, size_t N>
void print_array(T (&xs)[N]) {
std::cout << "[ ";
for (size_t i = 0; i < N; i++) {
std::cout << (i ? ", " : "") //
<< xs[i];
}
std::cout << " ]\n";
}
std::string a1[] = {"aa", "bb"};
std::cout << "a1 = ";
print_array(a1);
double a2[] = {3.14, 2.71828, 6.9};
std::cout << "a2 = ";
print_array(a2);
$ ./main.out | sed '1,/<2/d;/2>/,$d'
a1 = [ aa, bb ]
a2 = [ 3.14, 2.71828, 6.9 ]
Note that, since string literals are really an array of
const char, we can use the same technique to create template functions that know
the length of the string at compile time — provided it’s a literal:
template <size_t N>
void lit_strlen(const char (&)[N]) {
std::cout << "length = " //
<< N - 1 << "\n";
}
std::string a1[] = {"aa", "bb"};
std::cout << "a1 = ";
print_array(a1);
double a2[] = {3.14, 2.71828, 6.9};
std::cout << "a2 = ";
print_array(a2);
$ ./main.out | sed '1,/<3/d;/3>/,$d'
length = 5
length = 10
An important thing to note is that the length of the array will include
the null terminator, so the actual length — the number of
characters — will be N - 1.
Starting with C++20, we can also pass class types as template parameters; more formally, we can use a type as a non-type template parameter if it falls under one of these categories:
std::nullptr_tmutable
The categories above are collectively known as structural types. Prior to C++20, floating-point types and classes were not allowed, but now they are :D.
One of the things that people really wanted to do in C++17, but couldn’t do easily, was to pass strings as an NTTP. There were very limited ways that it could be done, but it was quite a pain so we won’t discuss it.
What we can do now in C++20 is to create a simple wrapper class that lets us pass strings around:
template <size_t Len>
struct String {
constexpr String(const char (&str)[Len]) {
std::copy(str, str + Len, m_buf);
}
template <size_t L2>
constexpr bool operator==(const String<L2>& s) const {
return Len == L2 //
&& std::equal(m_buf, m_buf + Len, s.m_buf);
}
template <size_t L2>
constexpr bool operator==(const char (&s)[L2]) const {
return *this == String<L2>(s);
}
char m_buf[Len];
};
Note that we had to make both the constructor and the
operator==
constexpr, otherwise the class would not be very usable at compile time.
This also shows that we can use templated classes as NTTPs. Then, we can create templated functions that can operate on these strings at compile time:
template <String s>
void foozle() {
if constexpr (s == "hello") {
std::cout << "hello to you too\n";
} else {
std::cout << "i don't understand\n";
}
}
foozle<String("hello")>();
foozle<String("aoeu")>();
foozle<"hello">();
$ ./main.out | sed '1,/<4/d;/4>/,$d'
hello to you too
i don't understand
hello to you too
In the last example here, note that we didn’t need to explicitly
construct the String — the compiler called the constructor
for us! This allows us greater flexibility in writing code that can
execute at compile-time, without being overly verbose.
For example, if we look at the godbolt link, the assembly looks something like this:
main:
push rbp
mov rbp, rsp
call void foozle<String<4ul>{...}>()
call void foozle<String<6ul>{...}>()
xor eax, eax
pop rbp
ret
void foozle<String<4ul>{...}>():
push rbp
mov rbp, rsp
movabs rdi, offset .L.str
call puts
pop rbp
ret
void foozle<String<6ul>{...>():
push rbp
mov rbp, rsp
movabs rdi, offset .L.str.1
call puts
pop rbp
ret
As we can see, there are no comparisons at runtime! We simply load the
string literal into the register, and call puts directly.
Reference: Explicit (full) template specialisation
Sometimes, we might want to customise the behaviour of a templated function for specific types or combination of types. Instead of individually checking each time in the body, we can just specialise the function.
We can rewrite the
if constexpr
checks above to use specialisation instead:
template <String s>
void bazzle() {
std::cout << "ich verstehe nicht\n";
}
template <>
void bazzle<"hello">() {
std::cout << "hello to you too\n";
}
template <>
void bazzle<"bye">() {
std::cout << "goodbye\n";
}
The caller looks exactly the same:
bazzle<"hello">();
bazzle<"bye">();
bazzle<"aoeu">();
$ ./main.out | sed '1,/<5/d;/5>/,$d'
hello to you too
goodbye
ich verstehe nicht
Now, we can call the correct function directly, without even doing any checks at compile time — the compiler does it for us when performing overload resolution.
Note that there’s another way to specialise function templates other than
the explicit (full) specialisation which we used for
bazzle<"hello">, which is to (indirectly) specify the type of T via the
parameter list:
template <typename T> // overload 1
void take_pointer(T p) {
printf("p(1) = %p\n", (void*) p);
}
template <> // specialisation of overload 1!
void take_pointer(void* p) {
printf("p(S1) = %p\n", p);
}
Note that specialised templates can have entirely different definitions and thus behaviours — though as part of good API design, they shouldn’t really differ too drastically. Some examples of when specialisation can be useful is to optimise the performance for a specific type, or when a particular value needs special handling because of how it is.
Function templates can also be overloaded, like this:
template <typename T> // overload 1
void take_pointer(T p) {
printf("p(1) = %p\n", (void*) p);
}
template <> // specialisation of overload 1!
void take_pointer(void* p) {
printf("p(S1) = %p\n", p);
}
template <typename T> // overload 2
void take_pointer(T* p) {
printf("p(2) = %p\n", (void*) p);
}
template <typename T> // overload 3
void take_pointer(const T* p) {
printf("p(3) = %p\n", (void*) p);
}
Note that this is distinct from specialisation; these are overloads. When resolving a function call, the compiler only considers overloads and the primary template. In this case, overloads 1-3, and not the specialisation.
Only after the primary template is selected does the compiler decide which specialisation to use (in this case, there’s only one).
One key caveat is that the order of definition matters here — especially so because we can overload function templates. The specialisation defined above specialises the first overload!
If we moved it after overload 2, then it would specialise the second overload, and so on.
The standard (and cppreference) have very confusing terminology, and use “specialisation” for both instantiations and user specialisations.
For our lectures, we refer to these as instantiations:
int x = foo<int, char>(69, 'x');
int y = foo(420, 'q');
Each call instantiates the template, generating one instantiation which is the function body with the template parameters replaced with their “real” arguments.
We refer to these as specialisations:
template <>
foo<int, char>(int, char) { /* ... */ }
They specialise the body of a template for a specific set of template arguments.
Again, the standard refers to both of these as specialisations, which is unnecessarily confusing, so we don’t do it.
So, how does the compiler choose which function to call? It decides via overload resolution, which is a very long page.
In short, the idea is that the compiler decides which template is
more specific, and chooses that one. In order to see which is
more specific, between each pair of function templates F1 and
F2, the compiler does the following:
F1 to a
unique hypothetical type
Xk
F2, without implicit conversions
F1 and F2 swapped
If, in step 2, F1 can match with F2 but not the
other way around, then F2 is more specialised than
F1, and vice versa. These set of rules naturally leads to the
following behaviour:
int) is more specialised than a template parameter (T)
T* is more specialised than T, because while
X* can match a T, X cannot match
a T*
const T is more specialised than T, because
while X can match a const T, the converse is
not true
If both F1 and F2 can match each other in step
2, it’s an ambiguous call and it’s an error.
Let’s look at an example:
template <typename T> // overload 1
void take_pointer(T p) {
printf("p(1) = %p\n", (void*) p);
}
template <> // specialisation of overload 1!
void take_pointer(void* p) {
printf("p(S1) = %p\n", p);
}
template <typename T> // overload 2
void take_pointer(T* p) {
printf("p(2) = %p\n", (void*) p);
}
template <typename T> // overload 3
void take_pointer(const T* p) {
printf("p(3) = %p\n", (void*) p);
}
const int x = 69;
take_pointer(&x);
take_pointer((void*) &x);
$ ./main.out | sed '1,/<1/d;/1>/,$d'
p(3) = 0x7fffffffebdc
p(2) = 0x7fffffffebdc
In the first call, we call take_pointer with
const int*. While it could have chosen overload 1 and deduced
T = const
int*, it instead chose overload 3 and deduced
T = int. This is an example of how the compiler orders function templates by
their specificity.
In the second call, we call it with
void*. While p(S1) is a perfect match for the argument
type, overload 2 is called instead. This is due to the rule we mentioned
above — the compiler only considers non-template functions and the
“primary” templates (in this case, overloads 1-3) in overload resolution.
Only after it chooses a given primary template does it check
which specialisation to use (if any).
Between overloads 1-3, overload 2 is the best match, so it is selected; it has no specialisation, so nothing special is done.
Note that the full rules for how this works together with the rest of overload resolution will be covered later (under… overload resolution).
Of course, class templates can also be specialised. One of the canonical
examples of specialising classes is in the
type_traits library. Suppose that we wanted to define a very
simple trait — is_int, which is true when the template
parameter is an
int, and false otherwise.
We can then do something like this:
template <typename T>
struct is_int //
: std::false_type {};
template <>
struct is_int<int> //
: std::true_type {};
static_assert(is_int<int>::value);
static_assert(!is_int<long>::value);
static_assert(!is_int<char>::value);
static_assert(!is_int<float>::value);
Here, we declared a template class is_int which inherits from
false_type in all cases, unless the template parameter is
int, in which case it inherits from true_type. Essentially, the
trait is “true” for ints and “false” otherwise.
We’ll cover the actual usage of true_type and friends later,
but they’re essentially just a struct with a value static
member, which is either
true or
false as their
names would suggest.
The important thing here is that we specialised is_int much
like we would specialise functions, and that the compiler picked the more
specialised struct correctly. This is an explicit specialisation,
which is denoted by
template <>, in which the full set of template parameters are specified and
specialised.
Reference: Partial template specialisation
Classes (and variables) with more than one template parameter can also be partially specialised, where at least one of the template parameters are left unspecialised (ie. as parameters), and one or more are specialised.
For example, here we have a simple Pair class, and we can
specialise it for cases where the first element is
int:
template <typename A, typename B>
struct Pair {};
template <typename B>
struct Pair<int, B> {};
The syntax here is similar to an explicit specialisation, just that we also have some template parameters. Of course, the parameters of the partial specialisation cannot be exactly the same as the original template.
For a less contrived example, we can implement an
is_array type trait that’s similar to our
is_int one. The issue here is that we need to handle all the
different possible array element types — which is completely infeasible to
do with explicit specialisations only (we’d need to list every single
element type — which is potentially limitless).
Instead, we can leave the element type as a template parameter:
template <typename T>
struct is_array //
: std::false_type {};
template <typename T>
struct is_array<T[]> //
: std::true_type {};
static_assert(!is_array<int>::value);
static_assert(!is_array<char>::value);
static_assert(is_array<int[]>::value);
using P = Pair<int, char>;
static_assert(is_array<P[]>::value);
is_array
This would work for all array types, since the element type is itself a
template parameter. In fact, this is how many of the type trait templates
in the standard library are implemented. If we look at the implementation
of std::is_array, we’ll see that this is exactly how it’s
implemented.
Just like how there is a partial ordering for function template specialisations, there is also a similar set of rules for partial specialisations. This ensures that the compiler will select the correct (more specialised) template when we have multiple specialisations.
Thankfully (kind of?), the rules for ordering here are just to transform each class partial specialisation into a (fictitious) function template, and then use the same rules for function template overload resolution.
First, let’s go back to our pair class, and add another partial specialisation; we also show the equivalent “fictitious function template”:
template <typename A, typename B>
struct Pair2 {
int x = 1;
};
/* transformed:
template <typename B> void f(Pair2<int, B>);
*/
template <typename B>
struct Pair2<int, B> {
int x = 2;
};
/* transformed:
template <typename A> void f(Pair2<A, char>);
*/
template <typename A>
struct Pair2<A, char> {
int x = 3;
};
Note that we also added a single field, so we can tell the specialisations apart. The rules themselves are relatively simple:
Note that explicit specialisations are preferred over partial specialisations.
Let’s try to instantiate various pair classes now:
Pair2<float, double> a{};
Pair2<int, double> b{};
Pair2<char, double> c{};
Pair2<float, char> d{};
std::cout << "a: " << a.x << "\n";
std::cout << "b: " << b.x << "\n";
std::cout << "c: " << c.x << "\n";
std::cout << "d: " << d.x << "\n";
$ ./main.out | sed '1,/<2/d;/2>/,$d'
a: 1
b: 2
c: 1
d: 3
As we would expect, a uses the primary (unspecialised)
template, b matches the first partial specialisation since
the first template argument is
int, c also uses the primary template, and d uses
the second partial specialisation.
Now, what would happen if we tried to instantiate
Pair2<int, char>? Would it pick the first specialisation or the second one?
$ make broken.out
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken.o broken.cpp
broken.cpp:17:20: error: ambiguous partial specializations of 'Pair2<int, char>'
Pair2<int, char> p{};
^
broken.cpp:7:8: note: partial specialization matches [with B = char]
struct Pair2<int, B> {
^
broken.cpp:12:8: note: partial specialization matches [with A = int]
struct Pair2<A, char> {
^
1 error generated.
make: *** [Makefile:25: broken.o] Error 1
The answer is neither, since it’s ambiguous. Note that instead, if we
added a full specialisation for
<int, char>, then it will get chosen in this case, and won’t be ambiguous:
template <>
struct Pair2<int, char> {
int x = 4;
};
Pair2<int, char> e{};
std::cout << "e: " << e.x << "\n";
$ ./main.out | sed '1,/<3/d;/3>/,$d'
e: 4
Reference: Parameter packs
If you’re familiar with C, you might know about its variadic functions
like printf and friends, allowing you to pass in any number
of arguments in the call.
Just as an exercise, let’s try to write a type-safe
println using C-variadic functions.
void println(const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
// ????????
va_end(ap);
}
Therein lies the problem — not only is there no way to figure out how many
arguments were actually passed in, we don’t even know their types! This is
completely
unsafe, and the reason that people discourage using something other than a
string literal as the format string, because that’s the only way
printf can know the types and number of arguments to expect.
Furthermore, we are forced to iterate the parameters at runtime, which can be costly.
This is where variadic templates save the day; we’ve already seen a brief example of them when implementing in-place construction in Lecture 6.
For this section, we’ll implement a simple type-safe alternative to
printf that uses variadic templates. Prior to the
introduction of variadic templates, we might have done
something (cursed)
like this:
void println(const char* fmt);
template <typename T1>
void println(const char* fmt, T1&& a1);
template <typename T1, typename T2>
void println(const char* fmt, T1&& a1, T2&& a2);
// ... oh no ...
Of course while this might end up working, it’s definitely not sustainable. What we want is to define a single function that can take in a varying number of arguments.
As we saw before, we can define a variadic template like this:
template <typename... Args>
void println(const char* fmt, Args&&... args);
We put ... after the
typename in
the template declaration, and in the function parameter list we append
... to the name of the template parameter. Note here that we
also used &&, which in this context makes each
parameter a forwarding reference.
There’s also one regular parameter —
const char* fmt
— which will be our format string.
The
typename... Args
part is known as a template parameter pack, and the
Args&&... args part is known as a
function parameter pack. A template with at least one parameter
pack is known as a variadic template.
Of course, this is not limited to functions — you can use template parameter packs on classes and variables too.
Note that every unique combination of types used to call a variadic
function will generate a new, separate instantiation of
the function. That is,
println('a', 97)
and
println(98, 'b')
will instantiate the function twice; This can be an issue if code size is
a concern.
However, this also means that the compiler knows the types of every argument at compile-time, and so it can better optimise the generated code. This is in contrast with C variadics, which can only be operated on at runtime.
One big issue in working with parameter packs is that you
cannot index into them; this is because each argument in the pack
might have a different type (remember, it’s as if each
typename were
an independent template parameter).
There are two common ways to use them, which we will cover here. The first way, and the only way pre-C++17, is to use recursion to “peel” off one parameter at a time from the parameter pack. We can do this like so:
void println(const char* fmt) { //
std::cout << fmt << "\n";
}
template <typename Arg1, typename... Args>
void println(const char* fmt, Arg1&& arg1, Args&&... rest) {
auto len = strlen(fmt);
for (size_t i = 0; i < len; i++) {
if (fmt[i] == '{' && i + 1 < len && fmt[i + 1] == '}') {
std::cout << std::string_view(fmt, i) //
<< arg1;
return println(fmt + i + 2, std::forward<Args>(rest)...);
}
}
}
Here, we recursively call println with the “rest” of the
arguments. Parameter packs can be empty, so if we call
println with at least one argument, we always get the second
overload; the first overload just serves as the base case.
Note the use of std::forward to correctly forward the
arguments’ value categories through the recursion here, as well as the use
of forwarding references (Args&&) to take in the
actual values.
The rest of the code is just to handle the simplest parsing of the “format
string”, which just looks for {} in the string.
int x = 69;
std::string s = "hello, world";
println("hello, {}, asdf, {}", x, s);
$ ./recursive1.out
hello, 69, asdf, hello, world
println implementation
One key point to note here is that we used ... to
expand the parameter pack. It behaves rather intuitively, with
the caveat that its placement affects the expansion you get. We placed it
outside of the call to std::forward, causing it to
expand to something like this:
// std::forward<Args>(args)... becomes
std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), ...
If we instead mistakenly placed it inside, we would expand to something like this:
// std::forward<Args>(args...) becomes
std::forward<Args>(arg1, arg2, ...)
This wouldn’t work because Args doesn’t get expanded — it
doesn’t appear in the expression preceding ..., which is only
args.
We can now use this function to print anything that has
operator<<
defined, like std::string, which is great.
Reference: Fold expressions
The other way to deal with parameter packs is to use fold expressions, which essentially applies an operator over every item in the parameter pack.
Let’s start by introducing a helper function that only prints one value:
template <typename Arg>
void print_one(const char*& fmt, size_t& len, Arg&& arg) {
for (size_t i = 0; i < len; i++) {
if (fmt[i] == '{' && i + 1 < len && fmt[i + 1] == '}') {
std::cout << std::string_view(fmt, i) << arg;
fmt += (i + 2);
len -= (i + 2);
}
}
}
Here, we are taking both the format string (fmt) and the
length (len) by reference, which will be important later. We
simply do the same checks for {}, then print the preceding
format string followed by the value itself.
We then adjust fmt and len to skip the part we
already printed (and the {}) — which affects the caller,
because they’re references.
Now, let’s look at the actual println function:
void println(const char* fmt) { //
std::cout << fmt << "\n";
}
template <typename... Args>
void println(const char* fmt, Args&&... args) {
auto len = strlen(fmt);
(print_one(fmt, len, std::forward<Args>(args)), ...);
std::cout << "\n";
}
println with fold expressions
That’s it! We simply pass in fmt and len to
print_one, do some weird syntax magic, and it works. Note
that we still had to define an overload that has no values; if the pack is
empty, then the fold expression expands to nothing. We can verify that the
output is still the same:
int x = 69;
std::string s = "hello, world";
println("hi");
println("hello, {}, asdf, {}", x, s);
$ ./fold1.out
hi
hello, 69, asdf, 69
println implementation (with fold
expressions)
Now, let’s look at fold expressions. They have 4 basic forms:
(Pack <op> ...) // unary right fold
(... <op> Pack) // unary left fold
(Pack <op> ... <op> init) // binary right fold
(init <op> ... <op> Pack) // binary left fold
Here, Pack denotes the name of the parameter pack, and
<op> is the operator that we want to fold over. Note
that only binary operators can be folded over — even in the unary
fold case.
If we let the pack values be a, b, ..., z, these folds would
expand to:
(Pack + ...) => (a + (b + (c + (... + (y + z)))));
(... + Pack) => (((((a + b) + c) + ...) + y) + z);
(Pack + ... + 0) => (a + (b + (c + (... + (z + 0)))));
(0 + ... + Pack) => (((((0 + a) + b) + c) + ...) + z);
In our case, we folded over the comma operator , using a
unary right fold. This means that the code expands to something like this:
print_one(fmt, len, std::forward<Arg1>(arg1)),
print_one(fmt, len, std::forward<Arg2>(arg2)),
print_one(fmt, len, std::forward<Arg3>(arg3)), ...;
print_one
This is why we made it take references, so that subsequent calls to
print_one can take the modified state (and further modify
it).
Another point to note is that the parameter pack can appear anywhere
inside the subexpression that you use in the folded expression, and
wherever it appears, the actual value is substituted. This is how we can
just use std::forward<Args>(args), and it automatically
works.
Finally, fold expressions can sometimes be faster (at runtime) than recursive expansion, simply because there isn’t a recursive call. The compiler is not always able to optimise out the recursive call even if all the types are known, so you should typically prefer to use fold expressions whenever possible.
There are other ways we can use fold expressions. For example, if we only wanted to make a function that printed its arguments without any format string capability, we could do this:
template <typename... Args>
void print_stuff(Args&&... args) {
(std::cout << ... //
<< std::forward<Args>(args)) //
<< "\n";
}
$ ./fold2.out
1hello3.1415
std::cout and
operator<<
in a fold expression
Of course the output is a little bad because there’s no spaces, but there’s ways to fix it (:
sizeof... operator
One small thing you can do is to use
sizeof...(pack)
to get the number of things in the parameter pack:
template <typename... Args>
void varargs_len1(Args&&... args) {
std::cout << "got " //
<< sizeof...(args) //
<< " arg(s)\n";
}
template <typename... Args>
void varargs_len2(Args&&... args) {
std::cout << "got " //
<< sizeof...(Args) //
<< " arg(s)\n";
}
varargs_len1(10, 20, 30);
varargs_len1();
varargs_len2("aaa");
$ ./misc.out
got 3 arg(s)
got 0 arg(s)
got 1 arg(s)
sizeof...
to get the number of items in a parameter pack
As you can see, just like normal
sizeof, we can use either the template parameter pack (the type,
Args) or the function parameter pack (the value,
args).
Reference: SFINAE
One last technique that we will cover is SFINAE — substitution failure is not an error. First, we must talk about what is substitution, and how it fails.
Types are substituted by the compiler, in:
decltype)
explicit
specifier (conditionally explicit)
Substitution happens in lexical order (ie. the order in which the things appear in the source code), and proceeds until the first failure.
A substitution failure happens when any of the types or expressions above would be ill-formed when the given template argument types are substituted. For instance:
template <typename T>
auto foo(const T& x) -> decltype(x.foo()) {
/* ... */
}
// later...
struct Bar {
int bar();
};
foo(Bar{});
Here, we tried to use x.foo() in the return type of the
function, but the template type (T = Bar) does not have such
a member function. In this case, we get a substitution failure.
This only applies to the immediate context of the template declaration — ie. the template parameters, parameter types, and return type. It does not apply inside the function body.
For instance, if we tweak the example above slightly:
template <typename T>
auto foo(const T& x) {
auto y = x.foo();
/* ... */
}
Here, the function’s immediate context doesn’t have any substitution
failures; if this template is chosen at instantiation time, then it will
produce a “real” error that stops compilation, because
Bar does not have a foo() method.
What happens when a substitution failure occurs? Well, the compiler simply removes the function that failed substitution from the overload set that is being considered. This means that in order to use SFINAE properly, you usually need at least 2 overloads of a function.
Let’s go back to our foozle example above; supposed that we
wanted to make it call the foo() method of the thing we pass
in, but if it doesn’t have such a method, it should just print an error
message at runtime.
We can make two overloads:
template <typename T>
auto foozle(T x) -> decltype(x.foo()) {
return x.foo();
}
void foozle(...) { //
puts("cannot foo()");
}
struct Foo {
void foo() {
puts("foo");
}
};
struct Bar {
int x;
};
foozle(10);
foozle(Bar{});
foozle(Foo{});
$ ./main.out | sed '1,/<1/d;/1>/,$d'
cannot foo()
cannot foo()
foo
We get the expected behaviour here. However, you might have noticed that
we used ... as the parameter type of the second overload. Why
is that? Because SFINAE only works one way — only disabling failing
substitutions. It does not only enable succeeding substitutions.
If we were a little naive and tried the “intuitive” second overload:
template <typename T>
void foozle(T) { //
puts("cannot foo()");
}
foozle(10);
foozle(Bar{});
foozle(Foo{});
$ make broken1.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken1.o broken1.cpp
broken1.cpp:32:3: error: call to 'foozle' is ambiguous
foozle(Foo{});
^~~~~~
broken1.cpp:17:6: note: candidate function [with T = Foo]
auto foozle(T x) -> decltype(x.foo()) {
^
broken1.cpp:23:6: note: candidate function [with T = Foo]
void foozle(T) { //
^
1 error generated.
make: *** [Makefile:25: broken1.o] Error 1
We see that the call with Foo as the argument is ambiguous,
because both the first and second overloads are viable, and are
equally good. As for why using the C variadic parameter
(...) works, we need to get into overload resolution, which
is covered below.
Let’s go back to our println function. It doesn’t really
matter which version you use, but we’ll use the one written with fold
expressions here. One major motivation of using SFINAE is in cases like
this:
// while these are legitimate uses...
println("hello {}", std::string("world"));
println("my fav number is {}", 420);
// if we misuse it,
// we get very long error messages
std::vector<int> v{1, 2, 3};
println("help: {}", v);
$ # This thing prints like
$ make demo-broken.out 2>&1 | wc -l
123
$ # that many lines of error messages
… ok, technically speaking, the large number of error messages is caused
by the large number of overloads that
operator<<
has, but the root cause of these error messages is because C++ is
type-checking the contents of
print_one<std::vector<int>>, even though it will obviously fail typechecking.
Clearly, this is a situation for just removing the function, and prevent it from instantiating the template in the first place. We can do this using the tools we just covered above:
template <
typename Arg,
typename = std::void_t<decltype(std::cout << std::declval<Arg>())>>
void print_one(const char*& fmt, size_t& len, Arg&& arg) {
for (size_t i = 0; i < len; i++) {
if (fmt[i] == '{' && i + 1 < len && fmt[i + 1] == '}') {
std::cout << std::string_view(fmt, i) << arg;
fmt += (i + 2);
len -= (i + 2);
}
}
}
// blatant misuse
std::vector<int> v{1, 2, 3};
println("help: {}", v);
$ make demo1.out
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o demo1.o demo1.cpp
demo1.cpp:29:4: error: no matching function for call to 'print_one'
(print_one(fmt, len, std::forward<Args>(args)), ...);
^~~~~~~~~
demo1.cpp:37:3: note: in instantiation of function template specialization 'println<std::vector<int> &>' requested here
println("help: {}", v);
^
demo1.cpp:11:6: note: candidate template ignored: substitution failure [with Arg = std::vector<int> &]: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'std::vector<int>')
void print_one(const char*& fmt, size_t& len, Arg&& arg) {
^
1 error generated.
make: *** [Makefile:25: demo1.o] Error 1
Here, we see that there’s a much shorter error message, and the compiler even helpfully tells us what the issue was — that the SFINAE caused this overload to be disabled. Since there were no other valid overloads, the compilation fails.
Using SFINAE, we ensured that print_one is only defined if
std::cout << x is well-formed (ie. it compiles
correctly), which means that an
operator<<
is present for the type we have.
println itself
One other solution would be to move the check to
println itself, which we can do in a very similar way:
template <typename... Args,
typename =
std::void_t<decltype(std::cout << std::declval<Args>())...>>
void println(const char* fmt, Args&&... args) {
auto len = strlen(fmt);
(print_one(fmt, len, std::forward<Args>(args)), ...);
std::cout << "\n";
}
$ make demo2.out
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o demo2.o demo2.cpp
demo2.cpp:37:3: error: no matching function for call to 'println'
println("help: {}", v);
^~~~~~~
demo2.cpp:26:6: note: candidate template ignored: substitution failure [with Args = <std::vector<int> &>]: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'std::vector<int>')
void println(const char* fmt, Args&&... args) {
^
demo2.cpp:18:6: note: candidate function not viable: requires single argument 'fmt', but 2 arguments were provided
void println(const char* fmt) { //
^
1 error generated.
make: *** [Makefile:25: demo2.o] Error 1
Here, we’re taking advantage of the fact that void_t
itself takes in any number of template arguments; we now just check that
all of them have
operator<<
defined, taking care to put the ... in the right place (after
the decltype).
This solution might be more desirable because the error message directly
pinpoints the caller of println, rather than an
implementation detail print_one which might be deep inside
some of your library code.
enable_if
Reference:
std::enable_if
What if we wanted to SFINAE away2
the function based on an arbitrary condition, not just “does it have
such-and-such method” or “is this expression well-formed”? Well, the
standard library has just the type for us — std::enable_if.
Its implementation is actually quite simple, and you can implement it yourself:
template <bool Cond, typename T = void>
struct enable_if {};
template <typename T>
struct enable_if<true, T> {
using type = T;
};
enable_if
It uses partial specialisation to work; if the condition is true, then we
choose the partial specialisation, and we get the type nested
type. If the condition is false, then we don’t get the nested type. Now,
we can apply the same SFINAE concepts we saw before; if
::type doesn’t exist, we get SFINAE and the overload is
discarded.
The type nested type is the second template parameter, which
is defaulted to void; you can also specify it if it needs to be an actual
type (depending on where you use it).
In fact, this is the basis of how type_traits work, though
we’ll look deeper into those later.
There are a few places we can put enable_if, and
we’ll go through all of these.
enable_if in
the return type
One place we can use enable_if is in the return type of
functions. For example:
template <typename T>
std::enable_if_t< //
std::is_integral_v<T>,
T>
square(T x) {
return x * x;
}
auto x = square(10);
auto y = square(20UL);
std::cout << "x = " << x //
<< ", y = " << y //
<< "\n";
enable_if in the return type
Note that this is one case where we have to specify the second template
parameter (well, if we wanted the function to return something other than
void).
You can use this placement for most functions (even if they return
void), but there are some where it’s not possible:
Note that if you need the enable_if condition to depend on
one of the parameter types (or values), you can use the
trailing return type declaration syntax, which we’ve seen above:
template <typename T>
auto cube(T x) -> std::enable_if_t<std::is_integral_v<decltype(x)>, T> {
return x * x * x;
}
enable_if with trailing return type
syntax
(Note that this is quite contrived, since decltype(x) is
exactly T).
enable_if as
a parameter type
The second place you can use enable_if is in the parameter
type. This is often used together with a default parameter, since in most
cases we don’t really care about the actual value.
Of course, you are free to use enable_if to declare a “legit”
parameter. Otherwise, one typical idiom is to make a default argument of
void*
type defaulted to
nullptr:
template <typename T>
auto pow(T x, T n, std::enable_if_t<std::is_integral_v<T>>* = nullptr) {
T ret = 1;
for (T i = 0; i < n; i++)
ret *= x;
return ret;
}
enable_if as a parameter type
Again, there are some situations where you cannot use this:
*,
--)
enable_if as
a template parameter
Finally, the last place you can use enable_if is in the
template parameter itself. Like with function parameters, this is
typically done as a defaulted parameter, like so:
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
auto half(T x) {
return x / 2;
}
enable_if as a (defaulted) template
parameter
This is generally the most flexible, since you can make any of the functions (constructors, conversion operators) templated. Note that we did not need to actually name the parameter here; since we don’t use it in the body, giving it a name is mostly pointless.
There is another way to use enable_if as a template
parameter, which is to make it the type of a NTTP, like so:
template <typename T, std::enable_if_t<std::is_signed_v<T>, int> foo = 0>
auto quarter(T x) {
return x / 4;
}
enable_if as a NTTP
Here, we used the second (usually defaulted to
void) parameter of enable_if to make an integer NTTP that’s
defaulted to 0. This works in much the same way as the method above, but
with a few benefits — which we’ll discuss immediately below.
enable_if
As much as enable_if (or more generally, SFINAE) is quite a
powerful construct, there are still some issues that you need to pay
attention to in order to use enable_if correctly.
The first issue is that two function templates with different defaulted template parameters are not considered distinct; they are not part of the function’s signature. For instance, if we had these two functions, the compiler would yell at us:
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
auto square(T) {
/* ... */
}
template <typename T, typename = std::enable_if_t<std::is_array_v<T>>>
auto square(T) {
/* ... */
}
$ make broken2.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken2.o broken2.cpp
broken2.cpp:11:34: error: template parameter redefines default argument
template <typename T, typename = std::enable_if_t<std::is_array_v<T>>>
^
broken2.cpp:6:34: note: previous default template argument defined here
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
^
broken2.cpp:12:6: error: redefinition of 'square'
auto square(T) {
^
broken2.cpp:7:6: note: previous definition is here
auto square(T) {
^
2 errors generated.
make: *** [Makefile:25: broken2.o] Error 1
enable_if
incorrectly
This is because we have essentially declared two functions that are equivalent. Note that the specific wording in the standard means that defaulted parameters are not considered when determining whether two templates are equivalent.
One way to fix this is to use the second method with non-type template parameters instead, like this:
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
auto rectangle(T, T) {
/* ... */
}
template <typename T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
auto rectangle(T, T) {
/* ... */
}
This works because now, the parameters have different types, so
they are not equivalent. Note that while the “real” type of both
NTTPs ends up being
int, their actual type is still
std::enable_if<...>::type, which is not the same since
the arguments to enable_if are still distinct.
As mentioned above, enable_if only serves to disable
overloads, and doesn’t say “only enable this overload”. This means that if
you have multiple SFINAE-ed overloads that are not completely mutually
exclusive, then it’s possible to get ambiguity errors.
For example, if we define two functions that happen to have some overlap:
template <typename T, std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
auto square(T x) {
return x * x;
}
template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
auto square(T x) {
return x * x;
}
// later...
square(10);
$ make broken3.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken3.o broken3.cpp
broken3.cpp:21:3: error: call to 'square' is ambiguous
square(10);
^~~~~~
broken3.cpp:7:6: note: candidate function [with T = int, $1 = 0]
auto square(T x) {
^
broken3.cpp:12:6: note: candidate function [with T = int, $1 = 0]
auto square(T x) {
^
1 error generated.
make: *** [Makefile:25: broken3.o] Error 1
enable_if can lead to ambiguous
overloads
Here, the issue was that the set of types fulfilling
is_arithmetic and is_integral have a non-empty
intersection — like
int that we
use. This only becomes a problem when you try to call the
function with a type that results in ambiguity, so it might not be
immediately apparent that there’s a bug here.
enable_if cannot be used to disable this declaration
One of the cryptic errors that you might face when using
enable_if is this one:
$ make broken4.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken4.o broken4.cpp
In file included from broken4.cpp:3:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/iostream:37:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/ios:214:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/__locale:15:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/string:520:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/__functional_base:15:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/__functional/invoke.h:14:
In file included from /usr/lib/llvm-13/bin/../include/c++/v1/__functional/weak_result_type.h:16:
/usr/lib/llvm-13/bin/../include/c++/v1/type_traits:550:78: error: no type named 'type' in 'std::enable_if<false>'; 'enable_if' cannot be used to disable this declaration
template <bool _Bp, class _Tp = void> using enable_if_t = typename enable_if<_Bp, _Tp>::type;
^~~
broken4.cpp:9:29: note: in instantiation of template type alias 'enable_if_t' requested here
template <typename = std::enable_if_t<std::is_same_v<T, int>>>
^
broken4.cpp:26:23: note: in instantiation of template class 'SimpleVector<float>' requested here
SimpleVector<float> ys{};
^
1 error generated.
make: *** [Makefile:25: broken4.o] Error 1
enable_if cannot be used to disable this
declaration
The offending code was this:
template <typename T>
struct SimpleVector {
template <typename = std::enable_if_t<std::is_same_v<T, int>>>
SimpleVector(T x) {
// do something
std::cout << "x = " << x << "\n";
}
SimpleVector(...) { //
std::cout << "...\n";
}
// ... the rest of it ...
};
// later...
SimpleVector<float> ys{};
The problem here is similar to the one we faced when doing
static_assert — here, the enable_if did not
depend on any template parameters, so at the point of instantiation,
SFINAE does not apply. We need the template parameter to be on the
constructor itself in order for this to work.
One way to get around this is to create a template parameter and default it, like so:
template <typename T2 = T,
typename = std::enable_if_t<std::is_same_v<T2, int>>>
SimpleVector(T x) {
// do something
std::cout << "x = " << x << "\n";
}
If we run it, we get the expected output (calling the second constructor):
SimpleVector<int> xs{10};
SimpleVector<float> ys{10};
$ ./not-broken5.out
x = 10
...
One common scenario is wanting to get the type-safety of C++ variadic templates, but not wanting each argument to be a possibly different type.
Unfortunately, the following isn’t quite possible:
void foo(int... ints) { /* ... */ }
While you can make a variadic template of only
int by using
non-type parameters like so:
template <int... Ints>
void foo() { /* ... */ }
This limits them to being constant expressions, which is usually undesirable. Instead, what we can do is to use our new SFINAE tricks to ensure that the function is not considered if the types differ.
First, since std::is_same only takes in two types, we need to
make some helpers that let us check if all the types we have are
the same:
template <typename T>
consteval bool is_same() {
return true;
}
template <typename T1, typename T2, typename... Ts>
consteval bool is_same() {
return std::is_same_v<T1, T2> && is_same<T2, Ts...>();
}
Note that we make use of the std::is_same type trait here
(explained more below), as well as a base case overload with only one
template argument (a type is the same as itself). We make these
constexpr so
that they can be used in the condition of an enable_if.
Now we can write the actual function. In this case, let’s say we want to make a function that only accepts integers:
template <typename... Args>
auto print_ints(Args&&... values)
-> std::enable_if_t<is_same<int, Args...>()> {
((std::cout << values << ' '), ...);
}
Now we can call it with any number of integers:
print_ints(69, 420);
print_ints(1, 2, 3, 4);
$ ./misc2.out
69 420 1 2 3 4 10 20 10 20 a b c
print_intsIf we try to call it with some values that are not all integers:
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o misc2-broken.o misc2-broken.cpp
misc2-broken.cpp:28:3: error: no matching function for call to 'print_ints'
print_ints(69, 420, 'a');
^~~~~~~~~~
misc2-broken.cpp:20:6: note: candidate template ignored: requirement 'is_same<int, int, int, char>()' was not satisfied [with Args = <int, int, char>]
auto print_ints(Args&&... values)
^
1 error generated.
make: *** [Makefile:25: misc2-broken.o] Error 1
print_ints with non-integer things
We even get a nice error message!
Furthermore, we can also extend this so that it can take in any
T, not just
int:
template <typename T, typename... Args>
auto print_stuff(Args&&... values)
-> std::enable_if_t<is_same<T, Args...>()> {
((std::cout << values << ' '), ...);
}
print_ints
Of course, in this case you need to manually specify the
T when calling print_stuff.
Reference: Metaprogramming library
You might have noticed along the way that we’ve been using a lot of
decltype, is_integral, is_pointer, and other such
things without really explaining what they are. Well, now we will explain
what they are.
We can group the metaprogramming library into a few groups:
These are also known as the “type traits”, since they can be used to discern the traits of a type.
We won’t list all of them (just go to the cppreference page for that), but we’ll cover some of the more useful and important traits.
These are themselves separated into primary categories and composite
categories. The primary ones are things like is_enum,
is_integral, is_pointer, etc., and they let you
test for the “primary category” of a given type. Note that some of them
are not possible to implement yourself, because they rely on compiler
support. For example, there’s no mechanism to determine whether a given
type is an enumeration or not.
The composite categories are simply combinations of the primary ones:
is_reference checks
is_lvalue_reference || is_rvalue_reference
is_arithmetic checks
is_integral || is_floating_point
These check the properties of the type; a few useful examples are
is_const, is_trivial, is_signed,
etc. A subcategory of type properties are the traits that let you check
the supported operations on a given type.
Those traits are most useful when writing generic container classes. For instance, you might want to use one implementation if the element type is copy assignable, and another when it is not. These traits also let you check whether the given operation is guaranteed not to throw an exception.
A few useful examples:
is_copy_assignable /
is_trivially_copy_assignable /
is_nothrow_copy_assignable
is_move_constructible /
is_trivially_move_constructible /
is_nothrow_move_constructible
is_swappable / is_nothrow_swappableThese let you check the relationship between two types. The most useful ones are:
is_sameis_base_ofis_convertibleis_invocableThe last category lets you transform (modify) an input type to produce an output type. Common examples include:
add_lvalue_reference / add_rvalue_reference /
remove_referenceadd_cv / add_const / … /
remove_cv / remove_const / …remove_cvrefremove_reference followed by remove_cv
common_typedecaystd::true_type and std::false_type
While std::true_type and std::false_type
are not exactly type traits, they are quite useful when
implementing traits of your own. Basically, in the true case, you
should inherit from true_type, and in the false case, you
should inherit from false_type. This just saves you the
trouble of making your own value member.
For instance:
template <typename T>
struct is_foozle : std::false_type {};
template <>
struct is_foozle<Foozle> : std::true_type {};
true_type and
false_type
std::void_t
We’ve already used this above; basically, the definition looks like this:
template <typename...>
struct void_t {};
std::void_t
It takes in any number of type arguments, and is essentially an easy way to check whether all of the arguments you give it are well-formed. In fact, that’s exactly how we used it above.
decltype
Another thing you might have seen us use without much explanation is
decltype. As the name might suggest, it allows you to get the type of the
expression that you pass in.
For instance,
decltype(1 +
2)
would be int. You can use
decltype
where a type would normally be expected, and we’ve seen some use of it in
type traits and SFINAE already.
This is a valid (but weird) use of
decltype, for instance:
template <typename T>
void foo(T x) {
decltype(x) y{};
}
decltype
decltype(x) vs decltype((x))
One important caveat here is the importance of an extra set of parentheses
when using
decltype. If we take this piece of code:
int x = 10;
static_assert(std::is_same_v<decltype(x), int>);
static_assert(std::is_same_v<decltype((x)), int&>);
decltype
We see that
decltype(x)
resolves to int, but
decltype((x))
resolves to
int&? What is this insanity?
The reason is that without the extra parentheses, we are saying “get the
type that the name x was declared with”. Since we wrote
int x =
..., then
decltype(x)
= int.
With the extra parentheses however, we are now saying “get the type of the
expression (x), and make it a reference depending on
its value category”. As we covered in a previous lecture, the value
category of a variable name as an expression is always an lvalue, so this
is why we get an lvalue reference here.
Specifically, the rules are:
T:
T&
T&&
Tstd::declval
You can think of std::declval as the cousin of
decltype; instead of giving you the type of something, it magically constructs a
value of a given type — even if the type is not constructible.
One typical use-case is when declaring (defaulted) template parameters that need to depend on previous parameters, like so:
template <typename T,
typename = std::enable_if_t<
std::is_same_v<decltype(std::declval<T>().foo()), int>>>
void kektus() {
/* ... */
}
std::declval
Here, we want to check that T has a method
foo that returns
int. What we might be tempted to do is something like
decltype(T{}.foo()), but this might not work if T is not default-constructible
for any reason. By using std::declval, we don’t even need to
call any constructors and we can magically get a value. As you can
imagine, this is usually useful when we want to check for methods.
Of course, you can’t actually call it! It’s only usable in unevaluated
contexts like inside a
decltype, because it can’t actually construct a “real” value for you.
Now that we know some of the common type traits, we need to know how to
use them. The traits usually come in two flavours: they “produce” either a
type or a value (typically a
bool).
Let’s cover the value-producing ones first. For instance, we might want to check whether a given type is an integral type:
template <typename T>
void foo() {
if constexpr (std::is_integral<T>::value) {
std::cout << "T is an integral\n";
} else {
std::cout << "T is not an integral\n";
}
}
struct Foo {
int x;
};
foo<int>();
foo<float>();
foo<Foo>();
$ ./main.out | sed '1,/<1/d;/1>/,$d'
T is an integral
T is not an integral
T is not an integral
Here, we instantiated is_integral with the type
T, and used the value static data member as the
“output” — which is either
true or
false.
This is a little verbose (especially with the extra ::value),
so in C++17 we can do this instead:
if constexpr (std::is_integral_v<T>) {
// ...
}
In fact, this is the form that should be most familiar, since most of our
prior examples were of this form. As we can see, we simply use
_v instead of ::value. The definition of these
helpers is actually quite simple:
template <typename T>
inline constexpr bool is_integral_v = std::is_integral<T>::value;
is_integral_v
The other class of traits are those that produce types like the
type-transforming traits, eg. remove_reference. For example,
let’s revisit our simple square function that we used
enable_if to SFINAE away earlier:
template <typename T,
typename = typename std::enable_if<std::is_integral_v<T>>::type>
T square(T x) {
return x * x;
}
typename =
typename
Well, this is definitely one way to do it. Note that because
type is a dependent type in this context, we have to
manually specify
typename
before naming the type so that the compiler knows that it should expect a
type.
This was not necessary for ::value because by default, the
compiler assumes that dependent names refer to values.
Of course, just like the _v helpers for value-producing
traits, we also have the similar _t helpers for
type-producing traits. In fact, we’ve already them before:
template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
T square(T x) {
// ...
}
Again, their implementation is quite trivial:
template <bool Cond, typename T = void>
using enable_if_t = typename std::enable_if<Cond, T>::type;
enable_if_t
Because of the way C++’s parsing works, the compiler needs to know whether
something is a type or a value when it encounters it; because
enable_if<T>::type is a dependent name, the compiler
doesn’t know, and we had to use
typename. For this _t case, it would already know that it refers to
a type, because it was declared with
using, so
typename is
unnecessary.
More formally, dependent names are names that depend on a template parameter. In particular, types and expressions may depend on template parameters — either type parameters or non-type parameters.
These are some common cases of dependent names appearing:
template <typename T>
struct Foo {};
template <typename T>
struct Bar : Foo<T> { // Foo<T> is a dependent name
typename Foo<T>::type* asdf = 0; // Foo<T>::type is a dependent name
int x = Foo<T>::value; // Foo<T>::value is a dependent name
};
There are two common situations where we run into dependent names.
We’ve already seen this situation before when using
enable_if<T>::type — we had to explicitly specify
typename, like so:
template <bool Cond, typename T>
using disable_if_t = typename std::enable_if<not Cond, T>::type;
typename
disambiguator for dependent names
The other common situation is when we have a dependent name and we want to use a template. For instance:
template <typename T>
struct Base {
template <typename U>
constexpr void uwu(U u) {
std::cout << "based? based on what?\n";
std::cout << "based on " << u << "\n";
}
};
template <typename T>
constexpr int owo() {
Base<int> b1{}; // b1 does not have a dependent type
Base<T> b2{}; // b2 has a dependent type
b1.uwu<int>(1); // ok, no `template` needed
b2.template uwu<int>(2); // `template` is needed
return 0;
}
template
disambiguator for dependent names
In the function, we make two variables, one with type
Base<int>, and one with
Base<T>
— the second one has a dependent type (depending on the template parameter
T).
When we call b1.uwu(), since the type of b1 is
already known, the compiler knows that uwu is a template
function, and can correctly parse the explicit template arguments
uwu<int>.
In the second case however, the type of b2 is dependent; in
order to prevent the compiler from parsing the < after
uwu as a less-than operator (since uwu could be
a non-static data member), we use the
template
keyword.
The arguments of the operators
sizeof, decltype, noexcept, as well as the contents of
requires
clauses, are considered unevaluated contexts, which means that
the contents are only used for their compile-time properties. This means
that, for example, the following code will not print anything:
auto x = sizeof(std::cout << "hello");
We previously mentioned that std::declval can only be used in
such a context — using it in a “normal” (ie. evaluated) context will
trigger a compile error.
Earlier, we showed how enable_if can be used wrongly when the
conditions used to enable a set of overloads are not mutually exclusive.
However, if we adapt the example to use C++20 concepts, we’ll see that it just works, even though the constraints overlap:
template <typename T>
requires std::integral<T> || std::floating_point<T>
auto square(T x) {
return x * x;
}
template <typename T>
requires std::integral<T>
auto square(T x) {
return x * x;
}
// later...
square(10); // this calls 2nd defn
square(1.0); // this calls 1st defn
$ make -B subsumption.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o subsumption.o subsumption.cpp
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable subsumption.o -o subsumption.out
So that illustrates one of the major motivations behind concepts. On top
of making it syntactically much easier to customise behaviour, (requires
is much nicer than std::enable_if), it also makes many other
corner cases Just Work.
In the rest of this section, we’ll cover both the language and library aspects of C++ concepts.
Let’s start with a simple example and breakdown.
// Using the `concept` keyword,
// we can declare a new concept
template <size_t I>
concept IsOdd = I % 2 == 1;
// Concepts are *very similar*
// to compile time booleans
template <size_t I>
constexpr bool IsOddBoolean //
= I % 2 == 1;
// But we'll see later that
// there are some differences
// We can now add requires clauses
// to declarations and definitions
// to constrain them
// Here, we constrain one definition
// to only become active when the I
// template parameter is odd
template <size_t I>
requires(IsOdd<I>) //
bool is_odd_comptime() {
return true;
}
// And here, we constrain the other
// to only become active when the I
// template parameter is even
template <size_t I>
requires(!IsOdd<I>) //
bool is_odd_comptime() {
return false;
}
Overall, it’s just a better SFINAE.
Instead of templating over a
typename T and
then constraining it with a concept, we can alternatively just write
constraint T, like so:
template <typename T>
requires std::integral<T>
void f(T x) {
++x;
}
// Alternatively:
template <std::integral T>
void f(T x) {
++x;
}
template <typename T>
requires std::convertible_to<T, int>
void f(T x) {
x + 1;
}
// Alternatively:
// T is prepended at the front of
// the list of template arguments
template <std::convertible_to<int> T>
void f(T x) {
x + 1;
}
Note the way that we left out the angle brackets for
std::integral, and the way that
std::convertible_to<int> is missing a template
argument. The template parameter being constrained is effectively
prepended to the list of template arguments and then the resulting
expression is used as the constraint.
requires expressions
Reference: Requires expression
Using type_traits to make new concepts is possible but very
annoying most of the time, especially when we usually just want to check
for basic things like whether expressions are well formed.
Using
requires
expressions, it’s much easier to create new concepts. They look like this:
requires (<similar to function parameters>) {
// ... requirements go here ...
}
and we can then put a variety of stuff inside.
For example, we might want to create a concept for whether a type can be
printed via std::ostream. In that case, we can make a concept
like so:
template <typename T>
concept Printable1 = requires (std::ostream& os, T obj) {
os << obj;
// Simply checks that the above expression would compile
};
requires
expression creates a concept
If we also want to check that the type of the expression is what we expect, we can use the following syntax:
template <typename T>
concept Printable2 = requires (std::ostream& os, T obj) {
// { ... } -> concept
// checks that ... is valid
// AND that the resulting type satisfies the concept
{ os << obj } -> std::same_as<std::ostream&>;
// Again note that `std::same_as` is missing a template argument,
// the same rules apply as the constrained template parameter case
// and the return type is prepended to the start,
// i.e. this is similar to adding the following constraint:
// std::same_as<decltype((os << obj)), std::ostream&>
};
The thing inside { } must be an expression, even
though the braces might suggest that it’s a normal block! That is, the
following is invalid:
requires(T a, U b) {
{ a + b; a - b } -> int;
}
One reason that the braces are required might be that for some expression
like x, a trailing -> could be interpreted as
a member access operator, whereas the addition of braces here can allow
the parser to parse this unambiguously.
What about concepts like the iterator concepts? For example,
we might want to check that a type contains all the member types required
to count as an Iterator. In this case, we use
typename <expr>
for the requirement.
template <typename T>
concept HasIteratorTraits = requires {
typename std::iterator_traits<T>::difference_type;
typename std::iterator_traits<T>::reference;
typename std::iterator_traits<T>::pointer;
typename std::iterator_traits<T>::iterator_category;
};
One very useful property of requires expressions is that they give us access to variables of a certain type, so we can easily build up expressions with them. We can then use this to build even bigger concepts that continue to have these variables in scope.
For example, the earlier Printable example could be
rewritten:
template <typename T>
concept Printable3 = requires (std::ostream& os, T x) {
os << x;
requires std::same_as<decltype((os << x)), std::ostream&>;
};
and in this case the nested requirement came in handy as it allowed us to
easily access the return type of
os << x.
While this was a little redundant in the earlier example, this can come in handy when we need to append the return type of an expression to a concept, or use the return type in the middle of a larger concept, rather than merely prepending it.
template <typename T>
concept IncrementAndMakeString_able = requires (T x) {
++x;
// Here, we want to *append* the return type of operator++
// so we know if a std::string can be constructed from a `++x`.
requires std::constructible_from<std::string, decltype((++x))>;
};
requires requires pattern
Since
requires
defines a concept and
requires
specifies a constraint, we can use both together to define a concept then
immediately apply it to constrain a definition.
template <typename It>
requires requires(It it) { ++it; }
It naive_advance(It it, size_t n) {
while (n != 0) {
--n;
++it;
}
return it;
}
requires
requires
in use
Generally, named concepts are preferred especially because they work better with constraint subsumption, which we discuss below.
println with concepts
Let’s look at our println function one last time. We
previously used SFINAE to disable the function if any of the types passed
in were not printable. However, the error message is still a little
obtuse, and we would like to make it very clear what the problem is.
We can do that with concepts, and specifically with the shorthand syntax we discussed above.
First, let’s make a concept that represents being Printable:
template <typename T>
concept Printable = requires(std::ostream& os, T x) {
os << x;
};
Printable concept
Next, we use the shorthand syntax to ensure that all the template arguments satisfy the Printable concept:
template <Printable... Args>
void println(const char* fmt, Args&&... args) {
auto len = strlen(fmt);
(print_one(fmt, len, std::forward<Args>(args)), ...);
std::cout << "\n";
}
Printable for each template argument
Of course, if we use it, it still works as expected:
int x = 69;
std::string s = "hello, world";
println("hi");
println("hello, {}, asdf, {}", x, s);
$ ./println.out
hi
hello, 69, asdf, 69
The real test is when we try to print something that isn’t printable.
We’ll use the same code snippet (printing a std::vector):
$ make println-broken.target
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o println-broken.o println-broken.cpp
println-broken.cpp:38:3: error: no matching function for call to 'println'
println("help: {}", v);
^~~~~~~
println-broken.cpp:30:6: note: candidate template ignored: constraints not satisfied [with Args = <std::vector<int> &>]
void println(const char* fmt, Args&&... args) {
^
println-broken.cpp:29:11: note: because 'std::vector<int> &' does not satisfy 'Printable'
template <Printable... Args>
^
println-broken.cpp:11:6: note: because 'os << x' would be invalid: invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'std::vector<int>')
os << x;
^
println-broken.cpp:25:6: note: candidate function not viable: requires single argument 'fmt', but 2 arguments were provided
void println(const char* fmt) { //
^
1 error generated.
make: *** [Makefile:25: println-broken.o] Error 1
println
While the errors are longer, they are more explicit —
std::vector does not satisfy the constraints, specifically
Printable, because os << x would be
invalid.
Since we’re talking about variadic templates, we can revisit our
print_ints and print_stuff functions from
before. Remember how we had to make some helper functions and use
enable_if to achieve our goals?
template <typename T>
consteval bool is_same() {
return true;
}
template <typename T1, typename T2, typename... Ts>
consteval bool is_same() {
return std::is_same_v<T1, T2> && is_same<T2, Ts...>();
}
template <typename... Args>
auto print_ints(Args&&... values)
-> std::enable_if_t<is_same<int, Args...>()> {
((std::cout << values << ' '), ...);
}
print_ints
With concepts, we can implement this in a better way:
template <std::same_as<int>... Args>
void print_ints2(Args&&... values) {
((std::cout << values << ' '), ...);
}
print_ints
By using
same_as<int>
to declare template parameter pack, we’re effectively constraining each
(expanded) template parameter to satisfy the
same_as<int> concept (recalling that the template is
prepended to the argument list, making
same_as<T1, int> etc.).
We can use a similar idea to extend this for print_stuff in
much the same way as we did for the original:
template <typename T, std::same_as<T>... Args>
void print_stuff2(Args&&... values) {
((std::cout << values << ' '), ...);
}
Of course, the error messages are nicer as well, as expected of concepts.
Reference: Partial ordering of constraints
In a previous lecture, we showed how we can use constraints to implement
customised range-based insert_back for our
SimpleVector class that made use of the iterator concepts.
template <typename InputIt>
// CONSTRAINT A
requires std::input_iterator<InputIt>
void insert_back(InputIt begin, InputIt end) {
// DEFINITION A
}
template <typename ForwardIt>
// CONSTRAINT B
requires std::forward_iterator<ForwardIt>
void insert_back(ForwardIt begin, ForwardIt end) {
// DEFINITION B
}
template <typename RandomIt>
// CONSTRAINT C
requires std::random_access_iterator<RandomIt>
void insert_back(RandomIt begin, RandomIt end) {
// DEFINITION C
}
However, what would happen if we passed in an iterator that satisfies the constraints for more than one definition? We saw that it would pick the “better” one:
ForwardIt it1, it2;
v.insert_back(it1, it2);
This call site satisfies the constraints for both definition A and B. In
this case, we can say that there are two viable candidates.
However, the constraint for definition A is
std::input_iterator, while the constraint for definition B is
std::forward_iterator. Because constraint A subsumes
constraint B, definition B is considered “better” than definition A. As a
result, this call site invokes definition B.
The reason why constraint B subsumes constraint A is because C++ is able
to prove that whenever constraint B is satisfied, constraint A is
satisfied. This makes sense, as every
std::forward_iterator is also a
std::input_iterator.
For the PL / AI / Algo nerds, rest assured that C++ is not solving SAT in the general case. It is simply following a dumb algorithm that handles the most obvious cases in a reasonably short amount of time, like the above example with iterator concepts.
Unfortunately for the same nerds, you’ll find that the description of the algorithm described on cppreference is still exponential time, when naively implemented. I don’t have a clue whether an efficient algorithm exists for this, but please tell me if you know. Thanks.
For normal people, it suffices to know that the following works:
template <typename T>
concept X = ...;
template <typename T>
concept Y = ...;
template <typename T>
concept A = X<T> && Y<T>;
template <typename T>
concept B = X<T>;
// In this case, A subsumes B
// because B has a subset of
// the clauses in A
template <typename T>
concept C = X<T> || Y<T>;
template <typename T>
concept D = X<T>;
// In this case, D subsumes C
// because D is one of
// the cases of C
Note that constraint subsumption is merely one of the rules in the more general overload resolution process.
Previously we mentioned that named concepts are preferred because of
subsumption — the reason is that ad-hoc constraints, even something as
simple as true, are never deduplicated by the
compiler, because they are considered atomic constraints.
Along with the concepts language feature, C++20 adds a substantial concepts library so as to make it easier for developers to transition to concepts.
Reference:
They mostly correspond to what we’ve already seen in
type_traits. It’s kinda boring to go through and explain what
everything means, so I’ll leave it here and let you read up on this own
your own.
Reference: Preshing - How C++ Resolves a Function Call
The last thing we will cover today is name lookup and overload resolution. While it sounds like the most obvious thing in the world, it is actually the most obvious thing in the world, which is why it’s so hard.
Let’s look at this example:
int min(int a, int b) { return a < b ? a : b; }
int main() {
// What happens if we call
min(1, 2);
// What about this?
min(1L, 2L);
// Does it compile?
}
The answer is that it compiles, and both call the
min function.
int min(int a, int b) { return a < b ? a : b; }
long min(long a, long b) { return a < b ? a : b; }
int main() {
// What happens if we call
min(1, 2);
// What about this?
min(1L, 2L);
// Does it compile?
}
min overloads
The answer is that it compiles, but the first call uses the first overload, and the second call uses the second overload.
int min(int a, int b) {
return a < b ? a : b;
}
long min(long a, long b) {
return a < b ? a : b;
}
int main() {
// What happens if we call this?
min(1, 2L);
// Does it compile?
}
The answer is that it no longer compiles.
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o mixed.o mixed.cpp
mixed.cpp:10:3: error: call to 'min' is ambiguous
min(1, 2L);
^~~
mixed.cpp:1:5: note: candidate function
int min(int a, int b) {
^
mixed.cpp:4:6: note: candidate function
long min(long a, long b) {
^
1 error generated.
make: *** [Makefile:25: mixed.o] Error 1
This is all “obvious”, because they’re exactly what we would expect. But things slowly begin to fall apart when we try to actually pin down what happens.
In the first example, we called
int min(int,
int)
with two
long
arguments, and it called the min function as expected. In
this case, the
longs were implicitly converted to
int, in order for it to work.
This is more common than you might think. For example, the following is an implicit conversion:
void print_greeting(std::string_view name) {
std::cout << "Oh hi " << name << std::endl;
}
int main() {
print_greeting("Mark");
}
const char[5]
is implicitly converted to std::string_view
In the second example, we had overloads for both
int and
long
arguments, so C++ “knew” what to do, and simply chose the better match.
In the third example, we provided one
int argument
and one
long argument.
This made C++ rather confused. Did we want it to implicitly convert the
int argument
to a long and
then call
long min(long,
long), or is it supposed to implicitly convert the
long argument
to an int and
then call
int min(int,
int)?
The earlier example was one where the arguments could convert to each
other. What about when the implicit conversion only goes in one direction?
For example, string literals (i.e. const char*
and
const char[]) can convert to std::string, but not the other way around.
#include <iostream>
#include <string>
void print_two_things(const char* thing1, const char* thing2) {
std::cout << thing1 << thing2;
}
void print_two_things(std::string thing1, std::string thing2) {
std::cout << thing1 << thing2;
}
int main() {
using namespace std::literals;
print_two_things("Oh hi ", "Mark"s);
}
This time, we see that it compiles fine. This is because we literally
cannot select the first overload anymore, so C++ can only call the second
overload. Once it decided that, it was okay with implicitly converting the
first string literal to a std::string.
What if we reduced code duplication by making it a template?
#include <concepts>
#include <iostream>
#include <string>
template <typename T>
concept Printable = requires(std::ostream& os, T x) {
{ os << x } -> std::same_as<std::ostream&>;
};
template <Printable T>
void print_two_things(T thing1, T thing2) {
std::cout << thing1 << thing2;
}
int main() {
using namespace std::literals;
print_two_things("Oh hi ", "Mark"s);
}
Now it complains!
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o mixed-template.o mixed-template.cpp
mixed-template.cpp:17:3: error: no matching function for call to 'print_two_things'
print_two_things("Oh hi ", "Mark"s);
^~~~~~~~~~~~~~~~
mixed-template.cpp:11:6: note: candidate template ignored: deduced conflicting types for parameter 'T' ('const char *' vs. 'std::string')
void print_two_things(T thing1, T thing2) {
^
1 error generated.
make: *** [Makefile:25: mixed-template.o] Error 1
In summary, overload resolution is one of those “Do What I Mean” C++ features. This is sometimes a good thing, since in many cases, things will Just Work (tm). However, it’s sometimes slightly less obvious what happens, and it’s not easy to make simple rules that describe what’s going on.
In order to get a clearer picture, I think it’s a good idea to get a bird’s eye view of how C++ resolves function calls.
Reference: Unqualified name lookup
The simplest form of lookup is where we simply name a function or variable
without any qualifiers (. or ::). When this
happens, lookup pretty much happens lexically: we look up the name from
the innermost scope to the outermost scope.
int x; // Found 4th, if the below declarations didn't shadow this one
namespace A {
int x; // Found 3rd, if the below declarations didn't shadow this one
struct B {
int x; // Found 2nd, if the below declaration didn't shadow this one
int f() {
int x; // Found 1st
x; // Unqualified name lookup happens here
}
};
} // namespace A
Occasionally there are some relatively sane exceptions, such as when looking up a scope in preparation for qualified name lookup, we might skip declarations that have the correct name because they aren’t the correct kind of declaration.
// This namespace named x is found
namespace x {
int y;
}
namespace A {
int x; // Skipped because `int` is not a scope
struct B {
int x; // Skipped because `int` is not a scope
int f() {
int x; // Skipped because `int` is not a scope
// Unqualified name lookup happens here for `x`
x::y;
// After the `x` scope is found, we look up `y` in that scope
// This second step is called qualified name lookup
}
};
} // namespace A
When we use the scope resolution operator :: or member access
operator ., we invoke qualified name lookup and member name
lookup respectively.
While unqualified name lookup looks in multiple scopes, qualified / member name lookup only inspects exactly the scope that you’re in.
int x; // (1)
namespace A {
int x; // (2)
struct B {
int x; // (3)
int f() {
int x; // (4)
// Qualified name lookup
::x; // Finds (1)
A::x; // Finds (2)
A::B::x; // Finds (3)
}
};
} // namespace A
A::B b{};
// Member name lookup
b.x; // Finds (3)
Here is where things start to get a little more complicated.
Recall that we can inherit from classes, and we can call functions and stuff in base classes.
struct A {
static int x; // (1)
void f(); // (2)
static int y; // (3)
};
struct B : A {
static int y; // (4)
};
B x{};
B::x; // Finds (1)
x.f(); // Finds (2)
B::y; // Finds (4)
They basically work as expected. Also note that when a derived class declares their own version of a name, it shadows the ones in the base classes.
So what happens when you have multiple inheritance, and you can find the same name in different base objects? The result is that the name lookup is ambiguous, and fails.
struct A {
static int x;
};
struct B {
static int x;
};
struct C : A, B {};
int main() {
// Errors due to ambiguous name lookup
C::x;
}
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o multi-inherit.o multi-inherit.cpp
multi-inherit.cpp:13:6: error: member 'x' found in multiple base classes of different types
C::x;
~~~^
multi-inherit.cpp:2:14: note: member found by ambiguous name lookup
static int x;
^
multi-inherit.cpp:6:14: note: member found by ambiguous name lookup
static int x;
^
multi-inherit.cpp:13:6: error: C++ requires a type specifier for all declarations
C::x;
^
2 errors generated.
make: *** [Makefile:25: multi-inherit.o] Error 1
This can even happen if the name appears in a single class, if they refer to different sub-objects due to multiple inheritance.
struct A {
static int x;
int y;
};
struct B : A {
static int z;
};
struct C : A {
static int z;
};
struct D : B, C {};
int main() {
// This is fine, A::x always refers to the same object!
// It refers to the object with static lifetime
// (only 1 copy in the entire program)
D::x = 0;
D d{};
// This is not fine. There are two y's in the memory layout of D.
// They would both be looked up, since neither shadows the other,
// nor are they both shadowed by a higher level declaration.
d.y = 0;
// This is also not fine, D::z will try both B::z and C::z,
// and they work, BUT point to different objects.
D::z = 0;
}
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o diamond-inherit.o diamond-inherit.cpp
diamond-inherit.cpp:29:5: error: non-static member 'y' found in multiple base-class subobjects of type 'A':
struct D -> struct B -> struct A
struct D -> struct C -> struct A
d.y = 0;
^
diamond-inherit.cpp:4:7: note: member found by ambiguous name lookup
int y;
^
diamond-inherit.cpp:33:6: error: member 'z' found in multiple base classes of different types
D::z = 0;
~~~^
diamond-inherit.cpp:8:14: note: member found by ambiguous name lookup
static int z;
^
diamond-inherit.cpp:12:14: note: member found by ambiguous name lookup
static int z;
^
diamond-inherit.cpp:33:6: error: C++ requires a type specifier for all declarations
D::z = 0;
^
3 errors generated.
make: *** [Makefile:25: diamond-inherit.o] Error 1
In a later lecture, we’ll show virtual inheritance, which allows us to “deduplicate” sub-objects, so sometimes this becomes okay again.
struct A {
int y;
};
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {};
int main() {
D d{};
// This is once again fine. With virtual inheritance,
// there is only one copy of the A subobject, so both
// d.B::y and d.C::y refer to the same subobject.
d.y = 0;
}
References:
The last aspect of name lookup that we’ll cover is argument-dependent lookup.
ADL applies only to unqualified function calls.
When ADL happens, it adds additional scopes to lookup the function name in, according to the types of the arguments. This primarily applies for types defined in namespaces (like libraries).
This is useful when you’re defining a helper function that has a common
name. std::swap is one such example. Here we’ll demonstrate
with print.
#include <iostream>
namespace MyLib {
struct S {
int n;
};
void print(S s) {
std::cout << "Lib: " << s.n << std::endl;
}
}; // namespace MyLib
// Imagine user also defines this
void print(int n) {
std::cout << "User: " << n << std::endl;
}
int main() {
print(1); // Prints "User: 1"
MyLib::print(MyLib::S{2}); // Prints "Lib: 2"
print(MyLib::S{3}); // Prints "Lib: 3". ADL was performed!
}
$ make -B example.out.run
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o example.o example.cpp
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable example.o -o example.out
./example.out
User: 1
Lib: 2
Lib: 3
A rough approximation of the way this works is to simply perform the lookup “nearby” where the type of the argument was defined.
In our example, we used a MyLib::S as the argument to
print. Since MyLib::print is defined in the same
innermost namespace as MyLib::S, it is added to the set of
candidates.
There are a couple other cases which are important for practically writing classes, namely comparison operators and swap. In this case, it’s advisable to define them using the hidden friend pattern, where we define the function / operator within the class, but as a friend function.
struct S {
int n;
friend auto operator<( //
const S& lhs,
const S& rhs) {
return lhs.n < rhs.n;
}
};
struct T {
int n;
auto operator<( //
const T& rhs) {
return n < rhs.n;
}
};
struct U {
int n;
U() {}
U(int n) : n(n) {}
auto operator<( //
const U& rhs) {
return n < rhs.n;
}
};
struct V {
int n;
V() {}
V(int n) : n(n) {}
friend auto operator<( //
const V& lhs,
const V& rhs) {
return lhs.n < rhs.n;
}
};
int main() {
(void) (S{} < S{});
// Works, while there is no operator< overload for S,
// ADL is performed for S and finds the friend function
(void) (T{} < T{});
// Works, < considers lhs.operator<
(void) (U{} < 1);
// Works, < considers lhs.operator<, which works because
(void) U{}.operator<(1);
// works as well (thanks to implicit conversions)
// (void) (1 < U{});
// Does NOT work, there is no `int.operator<`
// and ADL does not find member U::operator<
(void) (1 < V{});
// Works, because ADL finds friend operator< in S and V
// Out of these two overloads, only V's operator< is viable
}
It’s important to note that ADL applies only to unqualified function calls. This means that ADL is really a two stage process, where the first step is to perform ordinary lookup. If we find a function, or don’t find anything, etc., we apply ADL. If we find one of these three exceptions, then we don’t apply ADL and just stop there.
auto func_obj = [](int) {};
void f(int) {}
namespace A {
void f(int) {}
void g() {
f(1); // ADL
A::f(2); // No ADL
}
} // namespace A
struct S {
void f(int) {}
};
void g() {
S s{};
f(3); // ADL
A::f(4); // No ADL
s.f(5); // No ADL
func_obj(6); // No ADL
}
Note the last example.
func_obj(6)
has the exact same syntax as an ordinary function call, but it’s not
actually a function call, as it’s actually calling a function
object. Another way to think about this is that it’s calling
func_obj.operator()(6).
This can be tricky! For example, you might have the following:
#include <iostream>
#include <string>
auto swap = [](auto&, auto&) {
// Deliberately don't swap
};
int main() {
std::string a = "Hello";
std::string b = "world";
// Here, ordinary lookup first finds a *function object*
// So no ADL is performed!
swap(a, b);
// Prints Hello world
std::cout << a << ' ' << b << std::endl;
}
$ make -B shadowing-object.out.run
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o shadowing-object.o shadowing-object.cpp
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable shadowing-object.o -o shadowing-object.out
./shadowing-object.out
Hello world
Contrast that with the following example, where ADL is performed,
and we end up invoking the correct std::string swap:
#include <iostream>
#include <string>
void swap(auto&, auto&) {
// Deliberately don't swap
}
int main() {
std::string a = "Hello";
std::string b = "world";
// Here, ordinary lookup first finds a proper *function*
// So ADL *is* performed, and finds the std::string swap
swap(a, b);
// Prints world Hello
std::cout << a << ' ' << b << std::endl;
}
$ make -B shadowing-function.out.run
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o shadowing-function.o shadowing-function.cpp
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable shadowing-function.o -o shadowing-function.out
./shadowing-function.out
world Hello
References:
As we saw with the comparison operators earlier, it’s best practice to define them as a hidden friend. However, with what we just learnt, we now know that it’s possible such hidden friends aren’t found if ADL is disabled for some reason.
So if we want to make sure that ADL is performed, we add a
using
declaration that brings a function into the innermost scope, guaranteeing
that ordinary unqualified name lookup finds a function.
template <typename T>
void f() {
T a, b;
// SomeLib::swap(a, b);
// T is just a template parameter!
// What if the swap function for `T` is not in `SomeLib`?
// swap(a, b);
// Dangerous, what if there was a member named swap
// or something that would disable ADL?
// Add a `using` declaration to bring *some* function into scope
using std::swap;
swap(a, b);
// Really, any `using` of a function would enable ADL.
// But what we `using` will also serve as a fallback
// in the event that ADL finds nothing better,
// so you should `using` something that makes sense.
// Usually, this is `std`.
}
swap
One key point to note is that without
using std::swap, we can’t swap primitives! Since they don’t have a namespace, ADL
doesn’t apply, and we won’t find the correct swap function.
We need to bring the std::swap overloads into scope,
since they are the ones that define swap for primitives.
For example, this template will not work:
template <typename T>
void rearrange(T& a, T& b, T& c) {
swap(a, b);
swap(b, c);
}
// later...
namespace foo {
struct S { /* ... */ };
void swap(S& a, S& b) {
// ...
}
};
foo::S a{}, b{}, c{};
rearrange(a, b, c); // this works
int x = 0, y = 0, z = 0;
rearrange(x, y, z); // this won't work!
using std::swap
is necessary in a templated function
To fix it, we need to add
using std::swap
inside rearrange.
One quirk of dependent names is how they bind to declarations, and the behaviour can be quite surprising, especially if you’ve never heard about it before. Let’s look at this first, which has no dependent names:
void foo(double) {
std::cout << "foo(double)\n";
}
void test1() {
foo(10);
}
void foo(int) {
std::cout << "foo(int)\n";
}
void test2() {
foo(10);
}
// this comes after all the stuff above
test1();
test2();
$ ./binding.out | sed '1,/<1/d;/1>/,$d'
foo(double)
foo(int)
This may already be unintuitive, but in test2, we call the
second overload foo(int) because at the call site, its
declaration is available and overload resolution picks the better one. In
test1, only one declaration exists, and so we call that one.
This behaviour also applies inside template bodies, even though they are not really “evaluated” until they are instantiated! For non-dependent names, binding happens as though it happened “in” the template body. For example:
void foo(double) {
std::cout << "foo(double)\n";
}
template <typename T>
void template_test1() {
foo(10);
}
void foo(int) {
std::cout << "foo(int)\n";
}
// this comes after all the stuff above
template_test1<int>();
$ ./binding.out | sed '1,/<2/d;/2>/,$d'
foo(double)
Even though the instantiation of
template_test1 happens after the better overload
foo(int) was declared, it is still not chosen, and
we call the first overload, which is the only one that exists at the point
of template definition.
Well, lookup of dependent names in templates behaves the same way:
template <typename T>
void bar(double) {
std::cout << "bar(double)\n";
}
template <typename T>
void template_test2() {
bar<T>(10);
}
template <typename T>
void bar(int) {
std::cout << "bar(int)\n";
}
// this comes after all the stuff above
template_test2<int>();
$ ./binding.out | sed '1,/<3/d;/3>/,$d'
bar(double)
Except when argument-dependent lookup is involved: ADL allows finding declarations from the point of template instantiation. This is so that templated functions can find declarations that appear after its definition. For instance:
template <typename T>
void aaa(T) {
std::cout << "::aaa(T)\n";
}
template <typename T>
void template_test3() {
aaa(T{});
}
namespace nn {
struct S {};
void aaa(S) {
std::cout << "nn::aaa(S)\n";
}
} // namespace nn
// this comes after all the stuff above
template_test3<int>();
template_test3<nn::S>();
$ ./binding.out | sed '1,/<4/d;/4>/,$d'
::aaa(T)
nn::aaa(S)
Here, note that nn::aaa is successfully found, even though
its declaration appears later than the definition of
template_test3.
References:
After name lookup is done and we have a set of declarations, we have to
instantiate any templates as required, which sometimes involves type
deduction. We’ve been using it a lot without explanation, and it is the
mechanism by which the compiler figures out that
T = int
when you call
foozle<T>(T
x)
as
foozle(3), without you having to explicitly write
foozle<int>(3).
It is also the mechanism that lets you use templated operator overloads, since there’s no way to specify the template arguments in those situations.
The full mechanics of type deduction are quite complex, so we’ll cover only the less-esoteric stuff. For a more comprehensive explanation, you should watch the Scott Meyers talk above.
The first, and generally most common case, is for calling a template function. Firstly, after name lookup finds the list of declarations, for each template, we first perform deduction followed by substitution.
Deduction looks at each pair of argument/parameter, and essentially compares the arguments (caller) with the function’s parameters in order to deduce the types of any template parameters.
template <typename T>
void foo(ParamType);
foo(expr);
Given the type of the expression and the declaration of
ParamType, the compiler wants to figure out both
ParamType and T. After deduction is done for all
parameters, the compiler checks that the deduced types for all template
parameters agree; if there are conflicts, then deduction fails. If one or
more template parameters could not be deduced, deduction also fails.
There are 3 broad cases depending on the declaration of the parameter which we will cover.
Somewhat surprisingly, the simplest case is not just
T, but rather some non-forwarding reference to
T, like
T& or
const T&. Suppose we had the following function:
template <typename T>
void foo(T& param) {}
template <typename T>
void bar(const T& param) {}
void f1() {
int x = 0;
const int y = 0;
const int& z = x;
foo(x); // T = int, param = int&
foo(y); // T = const int, param = const int&
foo(z); // T = const int, param = const int&
bar(x); // T = int, param = const int&
bar(y); // T = int, param = const int&
bar(z); // T = int, param = const int&
}
Here, the rules are rather straightforward:
expr is a reference, ignore the reference
for deduction
T
Hopefully the examples above are sufficiently illustrative. When calling
bar, since the parameter is already declared
const, we can “shift” the const-ness from the deduced type T to
the parameter itself, so T does not need to be
const.
The same idea applies to functions taking pointers:
template <typename T>
void pfoo(T* param) {}
template <typename T>
void pbar(const T* param) {}
void f2() {
int x = 0;
const int* px = &x;
pfoo(&x); // T = int, param = int*
pfoo(px); // T = const int, param = const int*
pbar(&x); // T = int, param = const int*
pbar(px); // T = int, param = const int*
}
The second case is when the parameter is a forwarding reference. As a
brief refresher, a forwarding reference is a cv-unqualified rvalue
reference to a type T when T is a template
parameter of the immediately surrounding template.
For instance:
template <typename T>
void foozle(T&& forwarding_ref);
The rules are very similar to those of normal reference parameters,
except that if the expression (argument)’s value category is an
lvalue with some type E, then we deduce
T = E&. Using reference-collapsing rules, the actual type of the parameter
becomes
T& &&
= T&.
Again, it is important to note that forwarding references are not some special third kind of reference, but rather just a “hack” to make type deduction have this specific behaviour. This is the only case where template argument deduction deduces a reference type!
If we pass in an expression with an rvalue value category, then there’s no
special handling, and we deduce T = E, and the parameter type
becomes
E&& as
we would expect.
template <typename T>
void foozle(T&& param) {}
void f3() {
int x = 0;
int&& y = 0;
const int& z = x;
foozle(x); // T = int&, param = int&
foozle(y); // T = int&, param = int&
foozle(z); // T = const int&, param = const int&
foozle(7); // T = int, param = int&&
}
Note that we get
T = int&
(or
const int&) in the first 3 cases because even though y is an rvalue
reference, the value category of the expression y is an
lvalue!
The last category is deduction for by-value parameters:
template <typename T>
void kerfuffle(T param) {}
void f4() {
int x = 0;
int&& y = 0;
const int& z = x;
kerfuffle(x); // T = int, param = int
kerfuffle(y); // T = int, param = int
kerfuffle(z); // T = int, param = int
kerfuffle(7); // T = int, param = int
}
The rules are:
expr is a reference, ignore the reference
for deduction
const or
volatile
qualified, ignore that too
T is deduced to be whatever remainsThe rules are designed in such a way so that we get intuitive behaviour, since what we are semantically doing is creating a new object. That is, the reference-ness or const-ness of an initialising expression should not affect the type of the new object:
int x = 0;
const int cx = 10;
int& rx = x;
int z1 = cx; // z1 doesn't have to be const
int z2 = rx; // z2 doesn't have to be a reference
There are two special cases here for expressions (arguments) that are arrays or functions. If the parameter type is a reference, then we deduce an array or function type as appropriate.
However, in all other cases, we decay the argument and deduce a pointer type (pointer-to-element, or function-pointer) type instead.
The first rule (deducing array/function for references) is how we can pass array references to functions and be able to deduce the size of the array.
We’ve used
auto as a type
specifier a few times, and it’s a good way to reduce verbosity in your
code. In fact, using
auto in this
way has actually been invoking template argument deduction all along!
In short,
auto takes the
place of T in the rules we just described above, and in
particular
auto without
any const or
references uses by-value deduction rules.
void f5() {
int x = 10;
int& rx = x;
const int cx = 20;
const int& rcx = cx;
auto a1 = 10; // auto = int
auto a2 = rx; // auto = int
auto a3 = cx; // auto = int
auto a4 = rcx; // auto = int
auto& a5 = x; // auto = int, type = int&
auto& a6 = cx; // auto = const int, type = const int&
auto& a7 = rcx; // auto = const int, type = const int&
const auto a8 = x; // auto = int, type = const int
const auto a9 = cx; // auto = int, type = const int
const auto& a10 = x; // auto = int, type = const int&
const auto& a11 = rcx; // auto = int, type = const int&
auto&& a12 = x; // fwd-ref, auto = int&, type = int&
auto&& a13 = cx; // fwd-ref, auto = const int&, type = const int&
auto&& a14 = 10; // fwd-ref, auto = int, type = int&&
}
auto
deduction rules
When we use
auto in the
return type (deduced return type) for functions, we are also using
auto type
deduction, which is also template argument deduction.
std::initializer_list
There’s a special case for braced initialisers, which are like
{1, 2, 3}. In normal type deduction for function calls, these won’t deduce a type:
template <typename T>
void foozle(T) {}
int main() {
// this breaks
foozle({1, 2, 3});
}
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken1.o broken1.cpp
broken1.cpp:9:3: error: no matching function for call to 'foozle'
foozle({1, 2, 3});
^~~~~~
broken1.cpp:5:6: note: candidate template ignored: couldn't infer template argument 'T'
void foozle(T) {}
^
1 error generated.
make: *** [Makefile:25: broken1.o] Error 1
However, when we are using
auto, there are special rules; if we are doing
copy initialisation, then we deduce std::initializer_list<T> instead
(with T being the element type). For example:
auto xs = {1, 2, 3};
static_assert(std::is_same_v<decltype(xs), std::initializer_list<int>>);
However however, if we’re doing direct initialisation, then we do not get this special rule:
auto ys{2};
static_assert(std::is_same_v<decltype(ys), int>);
If we try direct initialisation with multiple elements, then it is a compile error:
// broken
auto xs{1, 2, 3};
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken2.o broken2.cpp
broken2.cpp:4:14: error: initializer for variable 'xs' with type 'auto' contains multiple expressions
auto xs{1, 2, 3};
~~~~~~~ ^
1 error generated.
make: *** [Makefile:25: broken2.o] Error 1
std::initializer_list
This is true from C++17 onwards — prior to that, both cases deduced
std::initializer_list; the change is due to paper
N3922.
One last kind of deduction that we’ll talk about is CTAD, which allows you to omit the template arguments when instantiating template classes. Since they are not functions, in typical cases we have to specify all of the template arguments, which can be a little annoying.
Starting from C++17, the compiler can perform deduction for classes too, letting us write something like this:
std::vector xs{1, 2, 3}; // deduces std::vector<int>
std::pair p{"a", 10}; // deduces std::pair<const char*, int>
The compiler essentially constructs fictitious functions that correspond to the constructors of the class, and uses the rules for function call deduction to deduce the template types for the class. These are known as implicitly-generated deduction guides, and as their name would suggest, they guide template deduction.
Note that if any explicit template arguments are specified, CTAD is not used.
Sometimes you might need to guide the compiler further, and to do that you can write user-defined deduction guides. For example, if we had the following simple class:
template <typename T>
struct Box {
T x;
};
We wouldn’t be able to call it without specifying the class:
// broken
Box box{10};
clang++ -g -stdlib=libc++ -std=c++20 -Wpedantic -Wall -Wextra -Wconversion -Wno-unused-variable -Wno-unused-but-set-variable -c -o broken3.o broken3.cpp
broken3.cpp:11:7: error: no viable constructor or deduction guide for deduction of template arguments of 'Box'
Box box{10};
^
broken3.cpp:4:8: note: candidate template ignored: could not match 'Box<T>' against 'int'
struct Box {
^
broken3.cpp:4:8: note: candidate function template not viable: requires 0 arguments, but 1 was provided
1 error generated.
make: *** [Makefile:25: broken3.o] Error 1
We can fix this by writing a deduction guide, like so:
Box(int)->Box<int>;
Box box{10};
std::cout << "box.x = " //
<< box.x << "\n";
$ ./ctad.out
box.x = 10
We can also make the deduction guides templated! One example here is the
constructor of std::vector that takes in an iterator pair; we
want the compiler to infer the correct element type (and not make a vector
of iterators), so we might do this:
template <typename Iter>
vector(Iter, Iter)->vector<std::iterator_traits<Iter>::value_type>;
There are some situations where the compiler cannot perform deduction; the types and values in that case use the previously deduced types/values for the template parameter.
Some examples:
// if 'T' appears in a nested-name specifier (to the left of any '::')
template <typename T>
void f7(typename std::type_identity<T>::type) {}
// this also counts, since `type_identity_t`
// also places 'T' to the left of a '::'
template <typename T>
void f7_1(std::type_identity_t<T>) {}
// if 'T' appears in a `decltype`
template <typename T>
auto f8() -> decltype(T{}.foo()) {}
// a non-type parameter that appears in a subexpression
template <size_t N>
void f9(char (&)[N * 2]) {}
Sometimes you might want to intentionally disable deduction for a
parameter — you can do this with the type_identity
type trait, which just returns the unmodified type. But since it places
T in a nested name, it is in a non-deduced context.
After type deduction is done (and succeeds), the compiler will attempt to substitute the deduced types into all their uses; this is where SFINAE comes into play. If substitution results in ill-formed code, then that particular overload of the function is discarded from the overload set.
After type deduction and substitution is done, we now have a set of candidate functions. Here’s an example of what we’ve covered so far:
namespace A {
struct S {};
void f(S, int); // (1)
} // namespace A
template <typename T>
void f(T, long); // (2)
// f specialization created by instantiation below
// template <>
// void f<S>(S, long); // (2A) (generated by template)
void f(int); // (3)
int main() {
A::S s{};
f(s, 1);
// Ordinary lookup finds:
// - (2) template <typename T> void f(T, long)
// - (3) void f(int)
// This is a function, so ADL is performed, and finds
// - (1) void S::f(S, int)
// Applying type deduction, we create an instantiation:
// - (2A) template <> void f<S>(S, long)
// The candidate functions are thus:
// - (1) void S::f(S, int)
// - (2A) template <> void f<S>(S, long)
// - (3) void f(int)
}
Once we have the set of candidate functions, we filter them to get a set
of viable functions, according to whether we would be allowed to call it
in the first place. In this case, we can’t call
void f(int)
with
f(s,
1), since the number of arguments don’t match.
After filtering out the non-viable candidates, we have a list of viable functions:
void S::f(S, int)
template <>
void f<S>(S, long)
Finally, the overloads are ranked pair-wise until we find a single overload that’s better than the rest.
To compare two overloads, we use this 12 stage process. While we won’t cover the details, we’ll show a few examples showcasing the more important ones.
The first and most important stage is the ranking of overloads based on the implicit conversions of their arguments.
Reference: Ranking of implicit conversion sequences
Let’s just look at an example:
void f(long, int, int); // (1)
void f(long, char, int); // (2)
int main() {
f(1, short(2), 3); // Calls (1)
// If we're calling (1), then the following conversions happen:
// - Integer conversion (int -> long)
// - Integer promotion (short -> int)
// - Exact match (int -> int)
// If we're calling (2), then the following conversions happen:
// - Integer conversion (int -> long)
// - Integer conversion (short -> char)
// - Exact match (int -> int)
}
Since we have:
We will rank (1) above (2).
They’re considered the worst implicit conversion sequence, according to this rule:
- A standard conversion sequence is always better than a user-defined conversion sequence or an ellipsis conversion sequence.
- A user-defined conversion sequence is always better than an ellipsis conversion sequence
Because of this property, it can be used as a fallback for
<type_traits>-style
constexpr
functions, like in
this random library I found on the internet.
C++ will prefer non-templates, followed by template overloads. Of the template overloads, C++ prefers more specialized template overloads.
void f(const int*); // (1)
template <typename T>
void f(const T*); // (2)
template <typename T>
void f(T*); // (3)
int main() {
int i = 0;
const int* pi = &i;
f(pi);
// After type deduction, all 3 overloads have the exact same signature:
// - void f(const int*);
// - template<> void f<int>(const int*);
// - template<> void f<const int>(const int*);
// So ranking of implicit conversions etc. would not make a difference.
//
// However, C++ will prefer (1) to (2) to (3),
// since (1) is not a template,
// and the template parameter for (2)
// is more specialized than for (3).
// (roughly: the pattern `const T*` is longer than the pattern for `T*`)
}
C++ prefers constrained functions over unconstrained functions. Of the constrained functions, it will prefer constraints that subsume other constraints.
#include <iostream>
template <typename T>
void f(T) {} // (1)
template <typename T>
void f(T) requires true {} // (2)
template <typename T>
void g(T) requires std::integral<T> {} // (3)
template <typename T>
void g(T) requires std::integral<T> && true {} // (4)
int main() {
f(1); // Prefers (2) then (1)
g(2); // Prefers (4) then (3)
}
© 11 July 2022, Thomas Tan, Ng Zhia Yang, All Rights Reserved
^