License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Frédéric Pennerath, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : advanced.md

Table of contents

SFINAE and type traits

C++11/C++14 offer new very powerful features for meta-programming thanks to SFINAE and type traits.

SFINAE: “Substitution Failure Is Not An Error”

Given a function template with several possible specializations, SFINAE allows to select which specialization should be instantiated using evolved criteria:

Principles:

A basic example:

// is_deref(const T&) is true if T::operator*() exists.
constexpr bool is_deref(...) { return false; } // By default operator* is not expected to exist

// decltype generates an error if *t is not defined.
template <typename T> 
constexpr auto is_deref(const T& t) -> decltype(*t, true) { return true; }
...
int val = 1;
int* ptr = &val;
std::cout << "is_deref(val) = " << is_deref(val) << std::endl; // Prints false
std::cout << "is_deref(ptr) = " << is_deref(ptr) << std::endl; // Prints true

Type traits

// Definition of enable_if in header <type_traits>
template<bool cond, typename T = void>
struct enable_if {
               // Default definition defines an empty struct that generates
};
 
template<class T>
struct enable_if<true, T> { 
  typedef T type;   // Type alias only declared when passed constexpr condition is true
};

// Example of usage of enable_if
template<typename T>
auto my_function(const T& t) -> typename std::enable_if<condition on T, int>::type { // Tries to access alias ::type (undefined if condition is false)
  // Function instanciated only if T satisfies the condition
}

Example showing how to use type traits, std::enable_if and std::result_of

// If type Callable returns void
template<typename Callable> 
typename std::enable_if<
  std::is_void<
    typename std::result_of<Callable>::type
  >::value,
  void
>::type 
operator() (const Callable& f){
  auto start = my_clock::now();
  f();
  auto end = my_clock::now();
  duration += (end - start);
}

// If Callable returns a type not void
template<typename Callable> 
typename std::enable_if<
  ! std::is_void<
    typename std::result_of<Callable>::type
  >::value, 
  typename std::result_of<Callable>::type
>::type 
operator() (const Callable& f) {
  auto start = my_clock::now();
  auto res = f();
  auto end = my_clock::now();
  duration += (end - start);
  return res;
}

This allows to instrument code as:

void f1() { ... }
}
int f2() { ... }
...
f1(); int res = f2();
// 
Chrono chrono;
chrono(f1); int res = chrono(f2);

Variadic templates (C+11)


A variadic template is a template (of a class or function) with a variable number of arguments.

Definition of a variadic class template

The standard library generalizes the class template std::pair to an arbitrary number of components through the variadic class template std::tuple:

Usage of std::pair and std::tuple

#include <utility> // Contains definition of std::pair

std::pair<int,bool> p1(3,true);
 
std::cout << "Value of p1 : " << p1.first << ',' << p1.second << std::endl;

auto p2 = std::make_pair(1.1f, "one");
#include <tuple> // Contains definition of std::tuple
  
std::tuple<int,bool,char,MyClass> t1(3,true,'c',MyClass());

std::cout << std::get<0,int,bool,char,MyClass>(t1);
// Or simply
std::cout << std::get<1>(t1) std::endl;

auto t2 = std::make_tuple(1.1f, "one", 'c');   // Before C++17
std::tuple t2 = { 1.1f, "one", 'c' };          // From C++17
           
// Retrieving components by structure binding operation
float x;
const char* s;
char c;

std::tie(x, s, c) = t2;   // Before C++17
auto [x, s, c] = t2;      // From C++17

Definition of std::tuple (simplified version)

// Declaration of a variadic template
template <typename... Elems> struct Tuple;

// Recursive specialization of the template
template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
  Head first;            // First component
  Tuple<Rest...> rest;   // Rest of components

  Tuple() : first(), rest() {} // Default constructor
};

// Terminal specialization of the template
template <>
struct Tuple<> { 
  Tuple() {}
};

// Let's instantiate it.
Tuple<int, double, char> t;

The code above is equivalent to:

class Tuple<int, double, char> {
  int first;
  Tuple<double, char> rest;
};

class Tuple<double, char> {
  double first;
  Tuple<char> rest;
};

class Tuple<char> {
  char first;
  Tuple<> rest;
};

class Tuple<> {
};

Notes:

Definition of a variadic function template

A function applied to a variadic class template requires to be recursive. Let’s consider the example of the constructor of previous class Tuple:

// Declaration of variadic class template
template <typename... Elems> struct Tuple;

// Recursive specialization of the constructor
template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
  Head first;            // First component
  Tuple<Rest...> rest;   // Rest of components

  ...
  Tuple(const Head& h, const Rest&... r) : first(h), rest(r...) {}
};

// Terminal specialization
template <> struct Tuple<> { 
  Tuple() {}
};

Notes:

A first example: defining output stream operator operator<< for class Tuple:

// Forward declaration of class Tuple
template <typename... Elems> struct Tuple;

template<typename Head, typename... Rest>
std::ostream& operator<<(std::ostream& os, const Tuple<Head, Rest...>& t) {
  std::cout << t.first << ',' << t.rest;
  return os;
}

// Terminal recursion
std::ostream& operator<<(std::ostream& os, const Tuple<>& t) {
  return os;
}

// To avoid an extra coma
template<typename Head>
std::ostream& operator<<(std::ostream& os, const Tuple<Head>& t) {
  std::cout << t.first;
  return os;
}

template <typename Head, typename... Rest>
struct Tuple<Head, Rest...> {
  ...
  // Declare both non empty operator<< as friends
  template<typename, typename...>
    friend std::ostream& operator<<(std::ostream&, const Tuple<Head, Rest...>&);
    
  template<typename>
    friend std::ostream& operator<<(std::ostream&, const Tuple<Head, Rest...>&);
  ...
};

A more complex example: implementing std::get on variadic class template Tuple

We will construct std::get step by step so that in the end, get returns the ith component, e.g

std::get<1>(std::make_tuple(1,'a',true)); // Returns 'a'

Step 1: declaration of variadic function template get

template<int index, typename... Elements> auto get(Tuple<Elements...>& t) {
  // To complete
}

Step 2: define a partial specialization

template<int index, typename Head, typename... Rest> 
struct TupleIter {
  static auto get(Tuple<Head, Rest...>& t) {
    // To complete
  }
};

We can now complete code of get:

template<int index, typename... Elements> auto get(Tuple<Elements...>& t) {
  return TupleIter<index, Elements...>::get(t);
}

Class TupleIter must be friend of class Tuple:

template <typename Head, typename... Rest>
class Tuple<Head, Rest...> {
  Head first;
  Tuple<Rest...> rest;
  ...
  template<int,typename, typename...> friend struct TupleIter;
}

Step3: use metaprogramming to implement method get in the general case

template<int index, typename Head, typename... Rest> 
struct TupleIter {
  static auto get(Tuple<Head, Rest...>& t) {
    return TupleIter<index - 1, Rest...>::get(t.rest);
  }
};

Step 4: finally make a partial specialization for the terminal case

template<typename Head, typename... Rest> 
struct TupleIter<0, Head, Rest...> {
  static Head get(Tuple<Head, Rest...>& t) {
    return t.first;
  }
};

Fold expressions (C++17)


Fold expressions facilitates the definition of a unary or binary operator to be applied to a parameter pack.

There exist 4 types of fold expressions as defined in the next table where:

args refers to a value pack a1, ..., an. f is a function (or function template) * Op is a binary operator. * v0 is an initial value.

Concept of Container
Fold Type Folded form Unfolded form
Unary right (f(args) Op ...) f(a1) Op (f(a2) Op ( ... f(an)))))
Unary left (... Op f(args)) ((((f(a1) Op f(a2)) ... ) Op f(an)
Binary right (f(args) Op ... v0) f(a1) Op (f(a2) Op ( ... (f(an) Op v0)))))
Binary left (v0 ... Op f(args)) ((((v0 Op f(a1)) ... ) Op f(an)

Here are two examples:

Application of a generic lambda function to a pack of arguments using unary right fold on operator operator,

template<typename Func, typename... Args>
void map(Func f, const Args&... args) {
  (f(args), ...);
}
...
int i = 0;
auto f = [&i] (auto& v) { std::cout << ++i << ") " << v << std::endl; };
map(f, 3, "abcd", 1.);

The resulting output is:

1) 3
2) abcd
3) 1

Binary left fold on operator operator+

template<typename I, typename... Values>
auto sum_of_squares(const I& init, const Values&... values) {
  return ((init * init) + ... + (values * values));
}
...              
std::cout << sum_of_squares(true, 2., 3) << std::endl; // Print 14 as 1^2 + 2^2 + 3^2 = 14
Frédéric Pennerath,