template: titleslide
???
We've hinted at this already, with std::vector<int>
, std::shared_ptr<Cowboy>
etc
Templates are a method of doing metaprogramming: a program that writes a program.
An easy example:
int sum(int a, int b) {
return a+b;
}
double sum(double a, double b) {
return a+b;
}
What if we need this for float
, unsigned
, and our Complex
class
from earlier lectures?
Going to get boring and hard to maintain quickly!
???
Recall the sum functions from the first lecture
template <typename T>
T sum(T a, T b) {
return a+b;
}
We can instantiate this template for a particular type explicitly by giving the type inside angle brackets:
std::cout << "add unsigned=" << sum<unsigned>(1U, 4U) << std::endl;
std::cout << "add floats=" << sum<float>(1.0f, 4e-2f) << std::endl;
???
Here the compiler will replace every use of T
with the type you
supply and only then will it compile the code
template <typename T>
T sum(T a, T b) {
return a+b;
}
You can also let the compiler deduce what T
is for you
std::cout << "add unsigned=" << sum(1U, 4U) << endl;
std::cout << "add floats=" << sum(1.0f, 4e-2f) << endl;
???
This is called implicit template instantiation - there are a few wrinkles that we'll talk about soon
You can define a template class - i.e. a template that will produce a class when you instantiate it.
Let's adapt the my_array
container to hold any type T
template<class T>
class my_array {
unsigned size = 0;
T* data = nullptr;
public:
my_array();
my_array(unsigned n);
// Copy / move?
~my_array();
unsigned size() const;
const T& operator[](unsigned i) const;
T& operator[](unsigned i);
};
??? Talk through the syntax of the template
We use T*
as the type of the stored data and T&
as the return type
of operator[]
Often it is useful to be able to create a new name for a type
C++ supports a using
declaration for this.
using MyImportantType = int;
using iter = std::map<std:string, std::vector<LongTypeName>>::iterator;
???
Really common when creating class templates as if you don't it may be very hard to figure out the type parameters it was instantiated for in from other code.
template<class T>
class my_array {
unsigned size = 0;
T* data = nullptr;
public:
using value_type = T;
using reference = T&;
using const_reference = T const&;
// ...
const_reference operator[](unsigned i) const;
reference operator[](unsigned i);
};
Templates are not executable code - they tell the compiler how to create it.
So the definition must be available in the translation unit of the user of your template - i.e. typically in a header file.
You can define the functions in place like:
template<class T>
class my_array {
public:
my_array() : _size(0), _data(nullptr) {}
};
Or at the end of the header (or equivalently in another file that you include at the end of your header)
template<class T>
my_array<T>::my_array(unsigned n) : _size(n) {
_data = new T[n];
}
??? Point out the uglier syntax of the second form but on the other hand the class definition shown earlier is cleaner with only the member function declarations
So I said earlier that everything used had to be defined exactly once.
This has two exceptions:
-
Templates
-
Functions marked
inline
These can be repeated in many "translation units" (i.e. separate invocations of the compiler)
At link time the linker will arbitrarily pick one definition to use in the final executable (so you need to make sure that they are all identical).
Recall:
template<class T>
T sum(T a, T b) {
return a+b;
}
We then used this without specifying, explicitly, what type T
was - e.g.:
int x = 1, y = 2;
auto z = sum(x, y);
The compiler is doing template argument deduction.
This means it examines the types of the expressions given as arguments
to the function and then tries to choose a T
such that the type of
the argument and the expected type match.
Important to note that the template parameter T
and the type of
function arguments might be different (but related)
template <class T>
void f(T x);
template <class T>
void ref(T& x);
template <class T>
void const_ref(T const& x);
template <class T>
void forwarding_ref(T&& x);
template <class T>
void vec(std::vector<T> x);
This will affect the deduced argument type
Can also parameterise template with non-types:
- integers
- pointers
- enums
- (and in C++20, floating point types and "literal types")
E.g.:
template <int N>
class Vec;
template <int ROWS, int COLS>
class Matrix;
template<int ROWS, int COLS>
Vec<ROWS> operator*(Matrix<ROWS, COLS> const& A, Vector<COLS> const& x);
??? The compiler will now ensure that the matrix and vector are of compatible shapes and if you make a mistake will give an error!
The size of the answer is correctly deduced for you
Full rules are quite complex
See Meyer's Effective Modern C++ chapter 1 - free online https://www.safaribooksonline.com/library/view/effective-modern-c/9781491908419/ch01.html
In short:
![:thumb]( But usually you can ignore these and just think about:
- Whether you want to copy the argument or not - if you don't want a
copy add a reference
&
- Whether you can handle a const argument - if so add a
const
qualifier - If you want exactly the type of the expression - if so add
&&
- this is known as a forwarding reference)
The auto
keyword follows the same rules as template argument
deduction
template: titleslide
-
Important C++ generic programming technique, used across the standard library
-
The "if-then-else" of types
-
Provide a template class that has typedefs/member functions that specify the configurable parts
-
Your generic algorithm then uses this class, specialised on the type it is working on, to select behaviour
-
You do not have to change the source of either the algorithm or the working type
Typically a trait is a template struct
that accepts one or more types as
parameter(s) and either:
.columns[
.col2[
Tells you something about the type(s) by producing a value, which is
stored in the value
static member variable, e.g.
if (std::is_pointer<T>::value) {
// do something pointery
} else {
// do something else
}
// From C++17 onwards:
static_assert(std::is_pointer_v<void*>);
]
.col2[
Transforms the type in some way by producing a type in the member
type alias type
.
using signed =
typename std::make_signed<
std::uint32_t
>::type;
// From C++14 onwards
using signed =
std::make_signed_t<std::uint32_t>;
] ]
Several headers contain these:
-
The header
<type_traits>
has lots of information for handling types. E.g.std::is_pointer<int>::value
has value of false. -
std::numeric_limits<T>
gives lots of parameters for the built in number types, such as largest and smallest values, whether they are integer or floating types, etc.
Other traits are used everywhere behind the scenes for efficiency.
Ideally you can define your own as a combination of existing traits.
If not you can investigate template specialisation, which is beyond the scope of this course.
Please try the second part of exercises/morton-order.