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

Table of contents

We now describe the main different types of templates. We give for each the syntax to use them and some illustrative usage scenario.

Class templates

Example 1: a class template with only types as arguments

Let’s consider the example of a vector in the plane parameterized by its type of scalar (float, double etc)

Example of declaration

#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];
}

Example of instanciation

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)
}

Remarks

Example 2: class template with values as arguments

Let’s now improve the vector example by adding the vector dimension as a second argument to the class template

Example of declaration

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];
}

Example of instanciation

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!!!
}

Fundamental property:

A template instance is a type uniquely defined by the values passed to its arguments whatever their type (type, constant values, etc).

Remarks:

The expert’s corner

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
}

Function, method and operator templates

A reminder about functions, methods and operators

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

Example 1: function template

Example of declaration : extraction of the minimum value in an array

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; } 
};

Example of instanciation

int main() {
  Vector<3> v = { 3., -1., 2.};
  std::cout << minimum<double>(v.begin(), v.end()) << std::endl; // Displays -1.
}

Example 2: external operator template

Exemple of declaration: output stream serialization operator

// 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>&);
};

Exemple of instanciation

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.
   ...
}

Notes

Example 3: method and internal operator templates

Exemple of declaration: vector concatenation method and vector assignement operator

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;
}

Exemple of instanciation

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
}

Notes

Variable templates

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. 
}

Alias templates

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)
}

Type inference and templates

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.

Importing types into templates: automatic type inference of template arguments

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.

Example

// 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; 
  ...
}

Notes

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*>

Exporting types from templates

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.

An example: a generic algorithm manipulating both matrices and vectors

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).

Solution based on type aliases

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

Type inference with C++11

With C++11, the compiler can automatically infer the type of:

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);
}

Type inference with C++14

C++14 generalizes the usage of auto:

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);
}

Advanced notions

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.

Frédéric Pennerath,