License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright :
Frédéric Pennerath,
CentraleSupelec
Last modified : February 14, 2024 14:00
Link to the source : templates.md
We now describe the main different types of templates. We give for each the syntax to use them and some illustrative usage scenario.
Let’s consider the example of a vector in the plane parameterized by its type of scalar (float
, double
etc)
#include <cassert>
#include <utility> // For std::pair
template<typename Scalar = double> class Vector {
Scalar coefs_[2]; // Internal array
public:
Vector(); // Initialize to null vector
Vector(const std::pair<Scalar,Scalar>&); // Construct from a pair of coefficients
Scalar& operator[](int); // To access vector components
};
template<typename Scalar> Vector<Scalar>::Vector() {
coefs_[0] = coefs_[1] = Scalar{};
}
template<typename Scalar> Vector<Scalar>::Vector(const std::pair<Scalar,Scalar>& pair) {
coefs_[0] = pair.first; coefs_[1] = pair.second;
}
template<typename Scalar> Scalar& Vector<Scalar>::operator[](int dim) {
assert(dim >= 0 && dim < 2);
return coefs_[dim];
}
int main() {
Vector<double> v1; // A 2D-vector of double
Vector<int> v2; // A 2D-vector of int
Vector<> v3; // A 2D-vector of double (same type as v1)
}
typename
could be replaced by class
(obsolete).assert
(declared in cassert
) checks conditions on runtime in DEBUG mode only (calls std::abort()
in case of failure)Let’s now improve the vector example by adding the vector dimension as a second argument to the class template
template<int dims=2, typename Scalar=double> class Vector {
Scalar coefs_[dims]; // Internal array
public:
Vector();
Vector(std::initializer_list<Scalar>) // Construction using an initialier list
Scalar& operator[](int); // To access vector coefficients
};
template<int dims, typename Scalar>
Vector<dims,Scalar>::Vector() {
for(Scalar* p = coefs_; p != coefs_ + dims; ++p) *p = Scalar{};
}
template<typename Scalar> Vector<Scalar>::Vector(std::initializer_list<Scalar> init) {
assert(l.size() == dim); // Static assert does not work :-(
Scalar *ptr = coefs_, *end = coefs_ + dims;
for(Scalar s : init) if(ptr == end) break; else *ptr++ = s;
}
template<int dims, typename Scalar>
Scalar& Vector<dims,Scalar>::operator[](int dim) {
assert(dim >= 0 && dim < dims);
return coefs_[dim];
}
int main() {
Vector<2,double> v1; // A 2D-vector of double
Vector<3,int> v2; // A 3D-vector of int
Vector<2> v3; // A 2D-vector of double (same type as v1)
Vector<> v4; // A 2D-vector of double (same type as v1)
Vector<double> v5; // Fails to compile!!!
}
A template instance is a type uniquely defined by the values passed to its arguments whatever their type (type, constant values, etc).
Vector<double,2>
, Vector<double,3>
and Vector<int,2>
are three different types (no conversion possible)dims
is a constant known at compulation time: array attribute coefs_
has a fixed size and can be embedded in Vector
instances. They can be stored on the stack (fast memory allocation).int
, char
, short
, long
, …enum Day {monday, tuesday, ...}
void (*my_function)(int,double&)
or void (*MyClass::my_method)(int,double&)
v5
does not compile).C++17 introduces the possibility to declare constant arguments for templates, with unspecified integral type (type auto):
template<auto uid> class Element {}; // Constant uid with any admissible integral type
const char name[] = "Albert"; // Global variable with a fixed static address
Element<name> elt1; // template <const char* uid> class Element;
int main() {
Element<23> elt2; // template <int uid> class Element;
Element<&elt1> elt3; // template <Element<const char*>* uid> class Element;
Element<&elt2> elt4; // Error: elt2 does not have a static address
}
C++ distinguishes four types of functions: function, method, external and internal operators.
/**********************/
/* Declaration syntax */
/**********************/
class MyClass; // Forward declaration
// 1) Function C (extern)
void f(const MyClass&) { ... }
// 2) External operator (cannot be virtual)
std::ostream& operator<<(std::ostream& os, const MyClass& obj) { ... }
class MyClass {
// 3) Method (interval function)
void g() { ... }
// 4) Internal operator (can be virtual)
MyClass& operator=(const MyClass& obj) { ... }
};
/***************/
/* Call syntax */
/***************/
MyClass c;
// 1) C-style function
f(c);
// 2) External operator
std::cout << c; // Call with an operator syntax style
operator<<(std::cout, c); // Equivalent call with a function syntax style
// 3) Method (internal function)
c.g();
// 4) Internal operator
c = MyClass(); // Call with an operator syntax style
c.operator=(MyClass()); // Equivalent call with a function syntax style
template<typename T> T minimum(const T* begin, const T* end) {
assert(begin != end);
T m = *begin;
for(const T* ptr = ++begin; ptr != end; ++ptr)
if(*ptr < m) m = *ptr;
return m;
}
template <int dims=2, typename Scalar = double>
class Vector {
...
public:
...
// Iterator factories begin() and end().
const Scalar* begin() const { return coefs_; }
const Scalar* end() const { return coefs_ + dims; }
};
int main() {
Vector<3> v = { 3., -1., 2.};
std::cout << minimum<double>(v.begin(), v.end()) << std::endl; // Displays -1.
}
// Forward declaration of Vector is required if operator<< is defined outside class Vector
template <int dim = 2, typename Scalar = double> class Vector;
template<int dims, typename Scalar>
std::ostream& operator<<(std::ostream& os, const Vector<dims, Scalar>& V) {
for(const Scalar* p = V.coefs_; p != V.coefs_ + dims; ++p) os << ' ' << *p;
return os;
}
template <int dims = 2, typename Scalar = double>
class Vector {
Scalar coefs_[dims];
public:
Vector();
Scalar& operator[](int);
friend std::ostream& operator<< <dims, Scalar>(std::ostream&, const Vector<dims, Scalar>&);
};
int main() {
Vector<3, double> v;
operator<< <3, double>(std::cout, v); // Works but ugly syntax
std::cout << <3, double> v; // Nice but this syntax is not accepted by the language.
...
}
Vector
must be predeclared (forward declaration) as an instance of function/operator template can only be declared friend in a class if that template has been previously declared.friend std::ostream& operator<< <dims, Scalar>(...)
states that ONLY this particular instance of the operator template is friend (not any instance of the template)We want: - To concatenate two vectors with the same scalar type, but possibly with different sizes - To assign a vector to another of the same size, but possibly with different scalar types
template<int dims, typename Scalar>
class Vector {
Scalar coefs_[dims];
public:
...
// Method template: concat two vectors
template<int dims2>
Vector<dims + dims2, Scalar> concat(const Vector<dims2, Scalar>&) const;
// Internal operator template: assignment operator
template<typename Scalar2>
Vector& operator=(const Vector<dims, Scalar2>&);
// Important: every instances of the Vector template must be friend alltogether
template<int dims2, typename Scalar2> friend class Vector;
...
};
# See the double template lines
template<int dims, typename Scalar> // Here are the arguments of the class template
template<typename Scalar2> // Here are the arguments of the method template
Vector<dims, Scalar>& Vector<dims, Scalar>::operator=(const Vector<dims, Scalar2>& v) {
// Declaration of Vector class template as friend is required here, in order to access private member coefs_ of v
const Scalar2* ptr2 = v.coefs_;
// Copy elements wit cast
for(Scalar* ptr = coefs_; ptr != coefs_ + dims; ++ptr, ++ptr2) *ptr = static_cast<Scalar>(*ptr2);
return *this;
}
template <int dims, typename Scalar>
template <int dims2>
Vector<dims + dims2, Scalar>
Vector<dims, Scalar>::concat(const Vector<dims2, Scalar>& v) const {
Vector<dims + dims2, Scalar> res; // Result vector
Scalar* dst = res.coefs_;
// Copy elements of current vector *this
for(const Scalar* ptr = coefs_; ptr != coefs_ + dims; ++ptr, ++dst) *dst = *ptr;
// Copy elements of argument vector v
for(const Scalar* ptr = v.coefs_; ptr != v.coefs_ + dims2; ++ptr, ++dst) *dst = *ptr;
return res;
}
int main() {
Vector<3,double> v1;
Vector<3,int> v2;
Vector<2,double> v3;
v2.operator=<double>(v1); // Assignment example
Vector<5,double> V = v1.concat<2>(v3); // Concat example
}
coefs_
in operator=
)concat
compute the size of the resulting vector dims + dims2
Vector<3,double>
with a vector of type Vector<2,double>
instantiates class Vector<5,double>
C++14 added the possibility to declare variable templates, with two important rules - two instances of the same variable template refer to the same variable in memory if and only if their arguments coincide. - variable templates only apply to variables instanciated at startup so that it only applies to: - global variables - static attributes of class
template<int dims, typename Scalar>
const Vector<dims, Scalar> zero {}; // zero is a template of global variables
int main() {
Vector<2,double> v1 = zero<2,double>; // Variable zero<2,double> is instanciated and copied to v1.
Vector<2,double> v2 = zero<2,double>; // There is no new instantiation as zero<2,double> has already been instanciated.
Vector<3,double> v3 = zero<3,double>; // zero<3,double> is a new variable.
}
The new type alias (based on keyword using
) replace the old ones (based on the C language keyword typedef
). They now accept alias templates:
template <int rows, int cols, typename Scalar> class Matrix;
using matrix22_t = Matrix<2, 2, double>;
// Equivalent to
typedef Matrix<2, 2, double> matrix22_t;
// But there is no typedef equivalence for:
template<int dim1, int dim2> using double_matrix_t = Matrix<dim1, dim2, double>;
template<int dim, typename Scalar=double> using squared_matrix_t = Matrix<dim, dim, Scalar>;
int main() {
matrix22_t A;
squared_matrix_t<2,double> B;
double_matrix_t<2,2> C;
A = B = C; // This is legal since A, B and C have the same type (only alias are differents)
}
Templates manipulate types at compile time: In some way we can think of types like “variables” processed by a program (the template) run at compile time in order to produce a result (the source code that will be compiled to produce binary code). Because of this, different mechanisms and techniques have been introduced in C++ to ease the manipulation of types.
The compiler can automatically infer the values of type arguments when instantiating a function/method/operator template if these values can be deduced by the types of values passed as arguments to the function.
// Just a reminder
template<typename T> T minimum(const T* begin, const T* end);
template<int dims, typename Scalar>
class Vector {
...
template<int dims2> Vector<dims + dims2, Scalar> concat(const Vector<dims2, Scalar>&) const;
}
int main() {
Vector<3,double> v1;
Vector<3,int> v2;
Vector<2,double> v3;
double min = minimum<double>(v1.begin(), v1.end());
// becomes
double min = minimum(v1.begin(), v1.end()); // T* is matched with double* so that T = double
Vector<5,double> V;
V = v1.concat<2>(v3);
// becomes
V = v1.concat(v3); // Matching : Scalar of v3 must be equal to Scalar of v1 (double) and dims2 must match dims of v3 (2)
v2.operator=<double>(v1)
// becomes
v2 = v1;
operator<< <5,double>(std::cout, V);
// becomes
std::cout << V;
...
}
make_pair
, make_tuple
, make_shared
, make_pair
, etc.int a = 1; double b = 0.;
std::pair<int,double> p{a, b}; // Ok
std::pair p{a, b}; // Nok
auto p = std::make_pair(a,b); // Ok
...
namespace std {
template< typename T1, typename T2>
std::pair<T1,T2> make_pair(const T1& v1, const T2& v2) {
return {v1, v2};
}
}
int a = 1; double b = 0.;
std::pair p{a, b}; // std::pair<int,double>
std::unique_ptr ptr{new std::thread{}}; // std::unique_ptr<std::thread>
std::tuple t{1,true,""}; // std::tuple<int, bool, const char*>
Arguments of template are only visible within the scope of the template (class, function, etc). But code instantiating these templates might need to know and manipulate values passed as arguments. This section presents different language features to do this.
Suppose we have developped a class template Matrix
whose interface is similar to our class template Vector
:
template<int dim1, int dim2, typename Scalar> class Matrix {
Scalar coefs_[dim1 * dim2];
...
};
One wants to share the algorithms that can be equally applied to matrices and vectors by writing function templates. For instance we consider the function template:
template<typename Scalar, typename Structure>
Scalar shift_positive_explicit(Structure& s) {
Scalar min = minimum(s.begin(), s.end()); // Reuse our function template minimum (see how automatic inference is used here)
s += (-min); // Shift the values of s so that the new minimum is 0.
return min;
}
To work, the way to access to coefficients of a vector or a matrix must be the same:
template<int dims, typename Scalar> class Vector {
public:
Vector<dims, Scalar>& operator+=(const Scalar&) { ... }
const Scalar* begin() const { return coefs_; }
const Scalar* end() const { return coefs_ + dims; }
...
};
template<int dim1, int dim2, typename Scalar> class Matrix {
public:
Matrix<dim1, dim2, Scalar>& operator+=(const Scalar&) { ... }
const Scalar* begin() const { return coefs_; }
const Scalar* end() const { return coefs_ + dim1 * dim2; }
...
};
This give:
int main() {
Vector<3,double> v;
Matrix<2,2,double> M;
double minV = shift_positive_explicit<double>(v); // Types of minV and of the template must be specified explicitely
double minM = shift_positive_explicit<double>(M);
...
}
The return type
double
must be made explicit when instantiating the template. In the general case, this is sometimes hard (nested types) or even sometimes impossible (generic algorithm).
The classical solution before C++11 was to export type arguments from templates by using type alias:
template<int dims, typename Scalar> class Vector {
public:
// scalar_type is visible outside class Vector
typedef Scalar scalar_type; // Syntax before C++11
using scalar_type = Scalar; // Syntax introduced by C++11
...
};
template<int dim1, int dim2, typename Scalar> class Matrix {
public:
typedef Scalar scalar_type; // Same for Matrix
...
};
This gives:
template<typename Structure>
typename Structure::scalar_type shift_positive_using_typedef(Structure& s) {
typename Structure::scalar_type min = minimum(s.begin(), s.end()); // See the typename keyword here
s += (-min);
return min;
}
Important: The keyword typename
must be added as a prefix to every type reference that uses a template argument (here Structure
).
It is then possible to write:
int main() {
Vector<3,double> V;
Matrix<2,2,double> M;
Vector<3,double>::scalar_type minV = shift_positive_using_typedef(V);
Matrix<2,2,double>::scalar_type minM = shift_positive_using_typedef(M);
...
}
Problem: the syntax is cumbersome with long type alias
With C++11, the compiler can automatically infer the type of:
auto
decltype
and the “trailing type” expressions:template<typename Structure>
auto shift_positive_Cpp11(Structure& s) -> decltype(minimum(s.begin(), s.end())) { // Declaration with a trailing type
auto min = minimum(s.begin(), s.end()); // Usage of auto for local variables
s += (-min);
return min;
}
int main() {
Vector<3,double> V;
Matrix<2,2,double> M;
auto minV = shift_positive_Cpp11(V);
auto minM = shift_positive_Cpp11(M);
}
C++14 generalizes the usage of auto
:
decltype
becomes useless):template<typename Structure>
auto shift_positive_Cpp14(Structure& s) { // Removal of trailing type
auto min = minimum(s.begin(), s.end());
s += (-min);
return min;
}
auto shift_positive_lambda = [] (auto& structure) {
auto min = minimum(s.begin(), s.end());
s += (-min);
return min;
}
int main() {
Vector<3,double> V;
Matrix<2,2,double> M;
auto minV = shift_positive_lambda(V);
auto minM = shift_positive_lambda(M);
}
Mechanisms underlying inference type are actually trickier and more subtle than what has been explained in this section. While the current section explains 90% of the usage cases encountered in practice, it cannot explain correctly the 10% remaining cases, those that can especially occur when developping libraries making an intensive usage of templates. If you are interested in mastering these cases as well, you can learn the more correct and ellaborated description of type inference in section Advanced notions.