CS100 Lecture 25
Templates I
Contents
- Function templates
- Class templates
- Alias templates, variable templates and non-type template parameters
Function templates
Motivation: function template
int compare(int a, int b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
int compare(double a, double b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
int compare(const std::string &a, const std::string &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
Motivation: function template
template <typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
- Type parameter:
T
- A template is a guideline for the compiler:
- When
compare(42, 40)
is called,T
is deduced to beint
, and the compiler generates a version of the function forint
. - When
compare(s1, s2)
is called,T
is deduced to bestd::string
, .... - The generation of a function based on a function template is called the instantiation (实例化) of that function template.
Type parameter
template <typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
- The keyword
typename
indicates thatT
is a type. - Here
typename
can be replaced withclass
. They are totally equivalent here. Usingclass
doesn't mean thatT
should be a class type.
Type parameter
template <typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
- Why do we use
const T &
? Why do we useb < a
instead ofa > b
?
Type parameter
template <typename T>
int compare(const T &a, const T &b) {
if (a < b) return -1;
if (b < a) return 1;
return 0;
}
- Why do we use
const T &
? Why do we useb < a
instead ofa > b
? - Because we are dealing with an unknown type!
T
may not be copyable, or copyingT
may be costly.T
may only supportoperator<
but does not supportoperator>
.
* Template programs should try to minimize the number of requirements placed on the argument types.
Template argument deduction
To instantiate a function template, every template argument must be known, but not every template argument has to be specified.
When possible, the compiler will deduce the missing template arguments from the function arguments.
template <typename To, typename From>
To my_special_cast(From f);
double d = 3.14;
int i = my_special_cast<int>(d); // calls my_special_cast<int, double>(double)
char c = my_special_cast<char>(d); // calls my_special_cast<char, double>(double)
Template argument deduction
Suppose we have the following function
template <typename P>
void fun(P p);
and a call fun(a)
, where the type of a
(ignoring references) is A
.
A
is never considered to be a reference. For example,
cpp
const int &cr = 42;
fun(cr); // A is considered to be const int, not const int &
Template argument deduction
Suppose we have the following function
template <typename P>
void fun(P p);
and a call fun(a)
, where the type of a
(ignoring references) is A
.
- Passing-by-value ignores references and top-level
const
qualifications. For example,
cpp
const int ci = 42; const int &cr = ci;
fun(ci); // A = const int, P = int
fun(cr); // A = const int, P = int
Template argument deduction
Suppose we have the following function
template <typename P>
void fun(P p);
and a call fun(a)
, where the type of a
(ignoring references) is A
.
- If
A
is an array or a function type, the decay to the corresponding pointer types takes place:
cpp
int a[10]; int foo(double, double);
fun(a); // A = int[10], P = int *
fun(foo); // A = int(double, double), P = int(*)(double, double)
Template argument deduction
Suppose we have the following function
template <typename P>
void fun(P p);
and a call fun(a)
, where the type of a
(ignoring references) is A
.
- If
A
isconst
-qualified, the top-levelconst
is ignored; - otherwise
A
is an array or a function type, the decay to the corresponding pointer types takes place.
This is exactly the same thing as in auto p = a;
!
Template argument deduction
Let T
be the type parameter of the template.
- The deduction for the parameter
T x
with argumenta
is the same as that inauto x = a;
. - The deduction for the parameter
T &x
with argumenta
is the same as that inauto &x = a;
.a
must be an lvalue expression. - The deduction for the parameter
const T x
with the argumenta
is the same as that inconst auto x = a;
.
The deduction rule that auto
uses is totally the same as the rule used here!
Template argument deduction
Deduction forms can be nested:
template <typename T>
void fun(const std::vector<T> &vec);
std::vector<int> vi;
std::vector<std::string> vs;
std::vector<std::vector<int>> vvi;
fun(vi); // T = int
fun(vs); // T = std::string
fun(vvi); // T = std::vector<int>
Exercise: Write a function map(func, vec)
, where func
is any unary function and vec
is any vector. Returns a vector obtained from vec
by replacing every element x
with func(x)
.
Template argument deduction
Exercise: Write a function map(func, vec)
, where func
is any unary function and vec
is any vector. Returns a vector obtained from vec
by replacing every element x
with func(x)
.
template <typename Func, typename T>
auto map(Func func, const std::vector<T> &vec) {
std::vector<T> result;
for (const auto &x : vec)
result.push_back(func(x));
return result;
}
Usage:
std::vector v{1, 2, 3, 4};
auto v2 = map([](int x) { return x * 2; }, v); // v2 == {2, 4, 6, 8}
Forwarding reference
Also known as "universal reference" (万能引用).
Suppose we have the following function
template <typename T>
void fun(T &&x);
A call fun(a)
happens for an expression a
.
- If
a
is an rvalue expression of typeE
,T
is deduced to beE
so that the type ofx
isE &&
, an rvalue reference. - If
a
is an lvalue expression of typeE
,T
will be deduced toE &
. - The type of
x
isT & &&
??? (We know that there are no "references to references" in C++.)
Reference collapsing
If a "reference to reference" is formed through type aliases or templates, the reference collapsing (引用折叠) rule applies:
& &
,& &&
and&& &
collapse to&
;&& &&
collapses to&&
.
using lref = int &;
using rref = int &&;
int n;
lref& r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref& r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&
Forwarding reference
Suppose we have the following function
template <typename T>
void fun(T &&x);
A call fun(a)
happens for an expression a
.
- If
a
is an rvalue expression of typeE
,T
is deduced to beE
so that the type ofx
isE &&
, an rvalue reference. - If
a
is an lvalue expression of typeE
,T
will be deduced toE &
and the reference collapsing rule applies, so that the type ofx
isE &
.
Forwarding reference
Suppose we have the following function
template <typename T>
void fun(T &&x);
A call fun(a)
happens for an expression a
.
- If
a
is an rvalue expression of typeE
,T
isE
andx
is of typeE &&
. - If
a
is an lvalue expression of typeE
,T
isE &
andx
is of typeE &
.
As a result, x
is always a reference depending on the value category of a
! Such reference is called a universal reference or forwarding reference.
The same thing happens for auto &&x = a;
!
Perfect forwarding
A forwarding reference can be used for perfect forwarding:
- The value category is not changed.
- If there is a
const
qualification, it is not lost.
Example: Suppose we design a std::make_unique
for one argument:
make_unique<Type>(arg)
forwards the argumentarg
to the constructor ofType
.- The value category must not be changed:
make_unique<std::string>(s1 + s2)
must moves1 + s2
instead of copying it. - The
const
-qualification must not be changed: Noconst
is added ifarg
is non-const
, andconst
should be preserved ifarg
isconst
.
Perfect forwarding
- The value category must not be changed:
make_unique<std::string>(s1 + s2)
must moves1 + s2
instead of copying it. - The
const
-qualification must not be changed: Noconst
is added ifarg
is non-const
, andconst
should be preserved ifarg
isconst
.
The following does not meet our requirements: An rvalue will become an lvalue, and const
is added.
template <typename T, typename U>
auto make_unique(const U &arg) {
return std::unique_ptr<T>(new T(arg));
}
Perfect forwarding
- The value category must not be changed:
make_unique<std::string>(s1 + s2)
must moves1 + s2
instead of copying it. - The
const
-qualification must not be changed: Noconst
is added ifarg
is non-const
, andconst
should be preserved ifarg
isconst
.
We need to do this:
template <typename T, typename U>
auto make_unique(U &&arg) {
// For an lvalue argument, U=E& and arg is E&.
// For an rvalue argument, U=E and arg is E&&.
if (/* U is an lvalue reference type */)
return std::unique_ptr<T>(new T(arg));
else
return std::unique_ptr<T>(new T(std::move(arg)));
}
Perfect forwarding
std::forward<T>(arg)
: defined in <utility>
.
- If
T
is an lvalue reference type, returns an lvalue reference toarg
. - Otherwise, returns an rvalue reference to
std::move(arg)
.
template <typename T, typename U>
auto make_unique(U &&arg) {
return std::unique_ptr<T>(new T(std::forward<U>(arg))).
}
Variadic templates
To support an unknown number of arguments of unknown types:
template <typename... Types>
void foo(Types... params);
It can be called with any number of arguments, of any types:
foo(); // OK: `params` contains no arguments
foo(42); // OK: `params` contains one argument: int
foo(42, 3.14); // OK: `params` contains two arguments: int and double
Types
is a template parameter pack, and params
is a function parameter pack.
Parameter pack
The types of the function parameters can also contain const
or references:
// All arguments are passed by reference-to-const
template <typename... Types>
void foo(const Types &...params);
// All arguments are passed by forwarding reference
template <typename... Types>
void foo(Types &&...params);
Pack expansion
A pattern followed by ...
, in which the name of at least one parameter pack appears at least once, is expanded into zero or more comma-separated instantiations of the pattern.
- The name of the parameter pack is replaced by each of the elements from the pack, in order.
template <typename... Types>
void foo(Types &&...params) {
// Suppose Types is T1, T2, T3 and params is p1, p2, p3.
// ¶ms... is expanded to &p1, &p2, &p3.
// func(params)... is expanded to func(p1), func(p2), func(p3).
// func(params...) is expanded to func(p1, p2, p3)
// std::forward<Types>(params)... is expanded to
// std::forward<T1>(p1), std::forward<T2>(p2), std::forward<T3>(p3)
}
Perfect forwarding: final version
Perfect forwarding for any number of arguments of any types:
template <typename T, typename... Ts> auto make_unique(Ts &&...params) {
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
This is also how std::vector<T>::emplace_back
forward arguments.
template <typename T> class vector {
public:
template <typename... Types> reference emplace_back(Types &&...args) {
if (size() == capacity())
reallocate(capacity() == 0 ? 1 : capacity() * 2);
construct_at(m_end_of_elems, std::forward<Types>(args)...);
++m_end_of_elems;
return *(m_end_of_elems - 1);
}
};
auto
and template argument deduction
We have seen that the deduction rule that auto
uses is exactly the template argument deduction rule.
If we declare the parameter types in an lambda expression with auto
, it becomes a generic lambda:
auto less = [](const auto &lhs, const auto &rhs) { return lhs < rhs; };
std::string s1, s2;
bool b1 = less(10, 15); // 10 < 15
bool b2 = less(s1, s2); // s1 < s2
auto
and template argument deduction
We have seen that the deduction rule that auto
uses is exactly the template argument deduction rule.
Since C++20: Function parameter types can be declared with auto
.
auto add(const auto &a, const auto &b) {
return a + b;
}
// Equialent way: template
template <typename T, typename U>
auto add(const T &a, const U &b) {
return a + b;
}
Class templates
Define a class template
Let's make our Dynarray
a template Dynarray<T>
to support any element type T
.
template <typename T>
class Dynarray {
std::size_t m_length;
T *m_storage;
public:
Dynarray();
Dynarray(const Dynarray<T> &);
Dynarray(Dynarray<T> &&) noexcept;
// other members ...
};
Define a class template
A class template is a guideline for the compiler: When a type Dynarray<T>
is used for a certain type T
, the compiler will instantiate that class type according to the class template.
For different types T
and U
, Dynarray<T>
and Dynarray<U>
are different types.
template <typename T>
class X {
int x = 42; // private
public:
void foo(X<double> xd) {
x = xd.x; // access a private member of X<double>
// This is valid only when T is double.
}
};
Define a class template
Inside the class template, the template parameters can be omitted when we are referring to the self type:
template <typename T>
class Dynarray {
std::size_t m_length;
T *m_storage;
public:
Dynarray();
Dynarray(const Dynarray &); // Parameter type is const Dynarray<T> &
Dynarray(Dynarray &&) noexcept; // Parameter type is Dynarray<T> &&
// other members ...
};
Member functions of a class template
If we want to define a member function outside the template class, a template declaration is also needed.
class Dynarray {
public:
int &at(std::size_t n);
};
int &Dynarray::at(std::size_t n) {
return m_storage[n];
}
template <typename T>
class Dynarray {
public:
int &at(std::size_t n);
};
template <typename T>
int &Dynarray<T>::at(std::size_t n) {
return m_storage[n];
}
Member functions of a class template
A member function will not be instantiated if it is not used!
template <typename T>
struct X {
T x;
void foo() { x = 42; }
void bar() { x = "hello"; }
};
X<int> xi; // OK
xi.foo(); // OK
X<std::string> xs; // OK
xs.bar(); // OK
No compile-error occurs: X<int>::bar()
and X<std::string>::foo()
are not instantiated because they are not called.
Member functions of a class template
A member function itself can also be a template:
template <typename T>
class Dynarray {
public:
template <typename Iterator>
Dynarray(Iterator begin, Iterator end)
: m_length(std::distance(begin, end)), m_storage(new T[m_length]) {
std::copy(begin, end, m_storage);
}
};
std::vector<std::string> vs = someValues();
Dynarray<std::string> ds(vs.begin(), vs.end()); // Values are copied from vs.
Member functions of a class template
A member function itself can also be a template. To define it outside the class, two template declarations are needed:
// This cannot be written as `template <typename T, typename Iterator>`
template <typename T>
template <typename Iterator>
Dynarray<T>::Dynarray(Iterator begin, Iterator end)
: m_length(std::distance(begin, end)), m_storage(new T[m_length]) {
std::copy(begin, end, m_storage);
}
Alias templates, variable templates and non-type template parameters
Alias templates
The using
declaration can also declare alias templates:
template <typename T>
using pii = std::pair<T, T>;
pii<int> p1(2, 3); // std::pair<int, int>
Variable templates (since C++14)
Variables can also be templates. Typical example: the C++20 <numbers>
library:
namespace std::numbers {
template <typename T>
inline constexpr T pi_v = /* unspecified */;
}
auto pi_d = std::numbers::pi_v<double>; // The `double` version of π
auto pi_f = std::numbers::pi_v<float>; // The `float` version of π
Non-type template parameters
Apart from types, a template parameter can also be an integer, an lvalue reference, a pointer, ...
Since C++20, floating-point types and literal class types with certain properties are also allowed.
template <typename T, std::size_t N>
class array {
T data[N];
// ...
};
array<int, 10> a;
Non-type template parameters
Define a function that accepts an array of any length, any type:
template <typename T, std::size_t N>
void print_array(T (&a)[N]) {
for (const auto &x : a)
std::cout << x << ' ';
std::cout << std::endl;
}
int a[10]; double b[20];
print_array(a); // T=int, N=10
print_array(b); // T=double, N=20
Summary
Idea of templates: Generic programming.
- Write code that can deal with different types. e.g. Containers parameterized on element types.
- The compiler generates (instantiates) the actual code in terms of the template code.
Function template:
- Template argument deduction:
- Pass-by-value: Reference and top-level
const
-qualification are ignored. Decay happens. - Pass-by-reference: Reference collapsing happens.
- Same as the deduction rule of
auto
.
Summary
Perfect forwarding:
- Forwarding reference:
auto &&
, orT &&
whereT
is a template type parameter. - Deduced to lvalue-reference if the argument is an lvalue, and rvalue-reference if the argument is an rvalue.
const
qualifications are preserved.std::forward<T>(x)
: Castsx
toT &&
.
Variadic template and pack expansion: Used to accept an unknown number of arguments of unknown types, and perform some operations on them. - "unknown" is for the author of the templates. The compiler generates the required versions of them.
Summary
Class templates:
- The class is parameterized on some template parameter. Different template arguments yield different classes.
- The member functions are instantiated only when they are called.
Other things:
- Alias templates
- Variable templates
- Non-type template parameters