Templates

MAP 579

Why templates ?


int min(int a, int b)
{
  return a < b ? a : b;
}

double min(double a, double b)
{
  return a < b ? a : b;
}
              

Same code for every arithmetic type

Possible solutions (1/3)

Use C macros


#define DEFINE_MIN(TYPE)  \
TYPE min(TYPE a, TYPE b)  \
{                         \
  return a <b ? a : b;    \
}

DEFINE_MIN(int)
DEFINE_MIN(double)
DEFINE_MIN(float)
....
#undef min
              

Possible solutions (2/3)

Use a common base class for all types


const object& min(const object& a, const object& b)
{
  return a.less_than(b) ? a : b;
}
              
  • limited type checking
  • No more value semantics (because of inheritance)

Possible solutions (3/3)

C++ templates: similar to C macros, but have it generated by the compiler

  • No need to write additional code for new types
  • No need to inherit for a common base class
  • Full type checking

Function templates

Function templates provide a function behavior that can be called for different types: a function template represents a family of functions.


template <class T> // T is called the "template parameter"
const T& max(const T& a, const T& b)
{
  return a <b ? a : b;
}
              

Or


template <typename T>
const T& max(const T& a, const T& b)
{
  return a <b ? a : b;
}
                

Function templates


int main()
{
  int i1 = 3, i2 = 5;
  std::string s1 = "math", s2 = "mathematics";
  double d1 = 2.4, d2 = 4.9;

  std::cout << ::max(i1, i2) << std::endl; // 5
  std::cout << ::max(s1, s2) << std::endl; // mathematics
  std::cout << ::max(d1, d2) << std::endl; // 4.9

  return 0;
}
              

Function templates


template <typename T>
const T& max(const T& a, const T& b)
{
  return a <b ? a : b;
}
              

int res = ::max(3, 5);
              

has the semantics of the following code:


int max(const int& a, const int& b)
{
  return a <b ? a : b;
}
                

Replacing template parameters by concrete types is called template instantiation.

Function templates


std::complex<double> c1, c2; // complex does not provide operator<
::max(c1, c2);               // ERROR at compile time
              

Templates are compiled twice:

  • Without instantiation, the template code itself is checked for correct syntax.
  • At the time of instantiation, the template code is checked to ensure that all calls are valid.

The compiler needs the definition of the template at the time of instantiation. This breaks the usual split of declaration and definition of ordinary functions.

Argument deduction


template <typename T>
const T& max(const T& a, const T& b);

max(4, 7); // OK, T is int for both argument
max(4, 3.2); // ERROR: first T is int, second T is double
              

Three ways to handle the error:


// Cast the arguments:
max(static_cast<double>(4), 3.2);
                

// Specify explicitly T:
max<double>(4, 3.2);
                

// Add more template parameters
template <class T1, class T2>
T1 max(const T1& a, const T2& b);
                

Argument deduction


template <class T1, class T2>
T1 max(const T1& a, const T2& b);
// Drawback: the first arugment defines the return type
              

template <class RT, clss T1, class T2>
RT max(const T1& a, const T2& b);
// Drawback: RT cannot be deduced:
double d = max<double>(4, 3.2); // OK, T1 and T2 are deduced
                

template <class RT = T1, class T1, class T2>
RT max(const T1& a, const T2& b);
double d = max(3.2, 4); // OK, T1 and T2 are deduced, RT is defaulted
                

template <class T1, class T2>
auto max(const T1& a, const T2& b) -> decltype(a+b) {...} // C++11

template <class T1, class T2>
auto max(const T1& a, const T2& b) { ... } // C++14
                

Argument deduction


template <class T>
void f(T t);

template <class T>
void  g(T& t);

int x = 42;
const int cx = 42;
const int& rx = cx;

f(x);  // What is T?
f(cx); // What is T?
f(rx); // What is T?

g(x);  // What is T?
g(cx); // What is T?
g(rx); // What is T?
              

Argument deduction


template <class T>
void f(T t);

f(x);  // x is int, T is int
f(cx); // cx is const int, T is int (const dropped)
f(rx); // rx is const int&, T is const int (reference dropped!)
              

template <class T>
void  g(T& t);

g(x);  // x is int, T is int
g(cx); // cx is const int, T is const int (const kept)
g(rx); // rx is const int&, T is const int (reference dropped)
              

Argument deduction

During template type deduction:

  • arguments that are references are treated as non-references
  • const arguments of by-value parameters are treated as non-const

Overloading function templates


inline int max(const int&a const int& b) { ... }

template <class T>
inline T max(const T& a, const T& b) { ... }

template <class T>
inline T max(const T& a, const T& b, const T& c) { ... }
              

For each of the following statemnts, which function is called?


::max(2, 5, 65);
::max(7., 20.);
::max('a', ' b');
::max(7, 42);
::max<>(7, 42);
::max<double>(7, 42);
::max('a', 42.7);
                

Overloading function templates


inline int max(const int&a const int& b) { ... }

template <class T>
inline T max(const T& a, const T& b) { ... }

template <class T>
inline T max(const T& a, const T& b, const T& c) { ... }
              

::max(2, 5, 65); // calls the template overload taking three arguments
::max(7., 20.);  // calls max<double> (argument deduction)
::max('a', ' b'); // calls max<char> (argument deduction)
              

::max(7, 42); // Calls the non template overload
              

::max<>(7, 42); // Calls max<int> (argument deduction)
::max<double>(7, 42); // Calls max<double> (no argument deduction)
              

::max('a', 42.7); // Calls the non template overload
              

Overloading function templates

When calling a function with many overloads:

  • The compiler will always choose the better match
  • All other factors being equals, a non template function is prefered over a template one
  • You can force the compiler to choose the template function with the <> syntax
  • Only the non template allows different argument types for a same parameter type

Class templates


template <class T>
class vector
{
  public:

    // C++98: typedef T& reference
    // C++11:
    using reference = T&

    vector();
    vector(const vector& rhs);

    reference operator[](size_t i);
    void push_back(const T& t);

  private:

    T* p_data;
    size_t m_size;
};
              

Class templates


template <class T>
vector<T>::vector() { // ... }

template <class T>
vector<T>::vector(const vector& rhs) { // ... }
              

template <class T>
typename vector<T>::reference vector<T>::operator[](size_t i)
{
  // ...
}
              

template <class T>
auto std::vector<T>::operator[](size_t i) -> reference
{
}
              

Class templates


template <class T>
void f(vector<T>& v)
{
  // Typename required:
  using reference = typename vector<T>::reference;
  // no need for typename:
  using double_reference = vector<double>::reference;
}

int main()
{
  vector<double> my_vec;
  f(my_vec);
  return 0;
}
              

Class templates


template <class T>
class my_class
{
  public:

    template<class U>
    void my_func(const U& u);
};

template <class T>
template <class U>
void my_class<T>::my_func(const U& u) { ... }
              

Template specializations


template <class T1, class T2>
class my_class
{
  public:
    void my_method(const T1& t1, const T2& t2);
};
              

// Partial specialization
template <class T>
class my_class<double, T>
{
  public:
    void my_method(double t1, const T& t2);
};

template <class T>
void myclass<double, T>::my_method(double t1, const T& t2) { ... }
              

Template specializations


// Full specialization
template <>
class my_class<int, double>
{
  public:
    void my_method(int t1, double t2);
};
              

// template<> // no template when defining members of full specialization
void my_class<int, double>::my_method(int t1, double t2) { ... }
              

Template specializations


template <class T1, class T2>
void my_func(T1, T2) { ... }

// OK:
template <>
void my_func<int, double>(int, double) { ... }

// Error:
template <class T>
void my_func<double, T>(double, T) { ... }
              

Function templates support full specialization only

Automatic differentiation - Part 1

In autodiff.hpp, define a template class "variable" that:

  • Has two members, one for the value, one for the derivative
  • Provides two methods to read these members
  • Provides a method to "activate" the variable (i.e. setting the derivative to 1)
  • Provides a constructor that accepts a single value and initializes the derivative to 0
  • Provides a template copy constructor and a template assignment operator

Automatic differentiation - Part 1

  • Implement a template binary_add class that:
    • Stores its two operands
    • Provides two methods for triggering the computation of the result and its derivative
  • Implement a template operator+ that instantiates and returns such a structure
  • Implement the template operator+= in the variable class based on the previous one

Automatic differentiation - Part 1

What is the issue with the previous design?

  • Implement a variable_add functor which provides an additional derivative method
  • Rename the binary_add structure into binary_op structure
  • Add a third template parameter to the structure (the functor describing the operation
  • Implement a variable_mul functor
  • Implement a template operator* and tempate operator*= for the variable class

Template template parameters


template <class E1, class E2>
class binary_add { // ... };

template <class F, class E1, class E2>
class binary_opĀ { // ... };

template <class E1, class E2>
binary_op<binary_add<E1, E2>, E1, E2>
operator+(const E1& e1, const E2& e2)
{
  return binary_op<binary_add<E1, E2>, E1, E2>(e1, e2);
}
              

Passing E1 and E2 to binary_add is cumbersome

Template template parameters


template <class E1, class E2>
class binary_add { // ... };

template <template <class, class> class F, class E1, class E2>
class binary_op
{
  public:
    using functor_type = F<E1, E2>
    // ....
};

template <class E1, class E2>
binary_op<binary_add, E1, E2>
operator+(const E1& e1, const E2& e2)
{
  return binary_op<binary_add, E1, E2>(e1, e2);
}
              

Traits

Traits are

  • small structures providing information about types
  • basics blocks of template metaprogramming

the standard header type_traits defines a lot of them (see documentation on cppreference.com)

Traits

Implementation pattern:


template <class T>
struct add_reference
{
  using reference = T&
};

// Specialization of add_reference for reference types
template <class T>
struct add_reference<T&>
{
  using reference = T&
};

template <class T>
using add_reference_t = typename add_reference<T>::type;
              

template <class T>
class my_vector
{
  using reference = add_reference_t<T>;
};
              

Traits

  • using declarations cannot be specialized
  • using declarations can be used without typename
  • structures can be specialized
  • structures requires to access an inner defined type, typename is required

We combine them to get the best of two approaches

Automatic differentiation - Part 1

We want to store the variables by references and the operation structure by value.

  • Implement a structure is_variable that defines an inner type as
    • std::false_type when its template parameter is NOT a variable
    • std::true_type otherwise
  • Implement a storage_type structure that defines an inner type as
    • its template parameter when it's not a variable
    • a const reference to its template parameter when it's a variable

Automatic differentiation - Part 2

  • Implement a unary_op structure, similar to binary_op
  • Implement functors for common math functions (exp, log)
  • Implement a template sigmoid function
  • Use your classes to compute both value and derivatives of the sigmoid

What is the issue with this design?

Variadic templates


template <class... T>
class universal_op
{
  public:
  using operands_types = std::tuple<T...>;
};
              

universal_op<int, double, float> u; // parameter pack is (int, double, float)
universal_op<int> u;                // parameter pack is (int)
universal_op<> u;                   // OK, parameter pack is empty
                

template <class... T, class E1>
class invalid {};
                

The parameter pack must be the last template parameter

Variadic templates


template <class... T>
void my_func(T... args)
{
  call_func(args...);
}

template <class... T>
void my_other_func(const T&... args)
{
  call_other_func(args...);
}
              

What if I want to pass some arguments by reference and other by values?

Universal references


template <class T>
void my_func(T&& t) { ... }

int x = 42;
int& rx = x;
const int& cx = x;

// For the following calls, what is T? what is the type of the argument of my_func?
my_func(x);
my_func(rx);
my_func(cx);
my_func(std::move(x));
my_func(42);
              

Universal references

During template type deduction with universal references:

  • arguments that are references are treated as references
  • T& && collapses to T&
  • T&& && collapses to T&&

Universal references


template <class T>
void my_func(T&& t) { ... }

int x = 42;
int& rx = x;
const int& cx = x;

my_func(x);   // T is int& - param type is int&
my_func(rx);  // T is int& - param type is int&
my_func(cx);  // T is const int& - param type is const int&
my_func(std::move(x)); // T is const int&& - param type is const int&&
my_func(42);  // T is int - param type is int&&
              

Universal references


template <class T>
void my_func(T&& t); // universal reference

tempate <class T>
void my_func(std::vector<T>&& v); // rvalue reference
              

Universal reference only for deduced types

Universal references


template <class T>
void my_func_impl(T&& t) { ... }

template <class T>
void my_func(T&& t)
{
  // calling my_func_impl with lvalue, wrong if t was an rvalue reference,
  my_func_impl(t);
  // calling my_func_impl with rvalue, wrong if t was an lvalue
  my_func_impl(std::move(t));
}
              

template <class T>
void my_func(T&& t)
{
  my_func_impl(std::forward<T>(t));
}
              

std::forward allows to achieve perfect forwarding

Universal references


template <class... T>
void my_func(T&&... args)
{
  // Forwards each argument, i.e. keeps the
  // lvalue-ness or rvalue-ness of each argument
  // indepently from other arguments
  call_func(std::forward<T>(args)...);
}
              

Curiously Recurring Template Pattern (CRTP)


template <class... T>
class universal_op;

template <class T>
class variable;

template <class E>
universal_op<exp, E> exp(const E& e)
{
  return universal_op<exp, E>(e);
}
              

exp catches any type wihle we would like to catch universal_op and variables only

Curiously Recurring Template Pattern (CRTP)


template <class D>
class expression
{
  public:
    D& operator()() { return *static_cast<D*>(this); }
    const D& operator()() const { return *static_cast<const D*>(this); }
};

template <class T>
class variable : public expression<variable<T>>
{ // ... };

template <class... T>
class universal_op : public expression<universal_op<T...>>
{ // ... };

template <class E>
universal_op<exp, E> exp(const expression<E>& e)
{
  return universal_op<exp, E>(e());
}
              

CRTP implements static polymorphism