Back to Basics: Templates Part 2

Back to Basics: Templates Part 2

Tags
c++
Published
November 5, 2020
Author
Chris Chan

Back to Basics: Templates Part 2

Video preview
 
These are my notes from the second part of Andreas Fertig 's talk from CppCon 2020 titled "Back to Basics: Templates".
 
 
 

Variadic Templates

Variadic templates allow you to pass a variable number of arguments to a template. It is denoted using three dots ....
In the example below, min continuously expands the items in the initializer list while finding the min until there are two items left.
template <typename T, typename... Ts> constexpr auto min(T& a, T&b, const Ts&... ts) { const auto m = a < b ? a : b; if constexpr(sizeof...(ts) > 0 ) { return min(m, ts...); } return m; } static_assert( min(3,2,3,4,5) == 2); static_assert( min(3,2) == 2);

Folded Expressions

Folded expressions are used to unpack a parameter pack using an operations. There are two four types of folded expressions listed below. The op denotes some operation of that is of the same type.
unary
  • right fold (pack op ...)
  • left fold (... op pack)
binary
  • right fold (pack op ... op init)
  • left fold (init op ... op pack)
In the example below, we use a unary left fold to unpack our parameter pack into std::cout.
template<typename T, typename... Ts> void Print(const T& targ, const Ts&... args) { std::cout << targ; aut coutSpaceAndArg = [](const auto& arg) { std::cout << ' ' << arg; }; (..., coutSpaceAndArg(args)); } Print("hello", "C++", 20);

Variable Templates

Using variable templates we can define constants such as pi or true_type. The example below illustrates the power of variable templates. Using the integral_constant struct we can define a true_type and a false_type. We then define two templates is_pointer that will derive from false_type or true_type depending on its input. In doing so, we can have a compile time check for the type of a variable.
template<class T, T v> struct integral_constant { static constexpr T value = v; }; using true_type = integral_constant<bool, true>; using false_type = integral_constant<bool, false>; // derive from integral_constant types above template<class T> struct is_pointer : false_type{}; template<class T> struct is_pointer<T*> : true_type {}; static_assert(is_pointer<int*>::value); static_assert(not is_pointer<int>::value);

SFINAE

Substitution failure is not and error (SFINAE) is a technique that gives the compiler hints as to how to compiler our code if its initial subsitution fails.
In this example our compiler decides on the function to instantiate not on the raw type of T, but rather on the result of std::is_floating_point_v.
template <typename T> std::enable_if_t<not std::is_floating_point_v<T>, bool> equal(const T& a, const T& b) { return a == b; } std::enable_if_t<std::is_floating_point_v<T>, bool> equal(const T& a, const T& b) { return std::abs(a - b) < 0.0001; } equal(2, 1); equal(2.0, 1.0);

Tag Dispatch

Tag dispatch is an alternative to SFINAE. In this technique, we define empty class tags and pass them to our functions as additional parameters. In doing so, we can overload functions with identical parameters.
In the example below, we define two empty structs notFloatingPoint and floatingPoint which we pass to the equal function to distinguish between the two.
namespace internal { struct notFloatingPoint {}; struct floatingPoint {}; template <typename T> bool equal(const T& a, const T& b, notFloatingPoint) { return a == b; } template <typename T> bool equal(const T& a, const T& b, notFloatingPoint) { return std::abs(a - b) < 0.0001; } } // namespace internal template<typename T> bool equal(const T& a, const T& b) { using namespace internal; if constexpr (std::is_floating_point_v<T>) { return equal(a, b, floatingPoint{}); } else { return equal(a, b, notFloatingPoint{}); } }

C++20 Concepts

Using Concepts in C++20, we can use the requires keyword to replace SFINAE.
template<typename T> requires(not std::is_floating_point_v<T>) bool equal(const T& a, const T& b) { return a == b; } template<typename T> requires(std::is_floating_point_v<T>) bool equal(const T& a, const T& b) { return std::abs(a - b) < 0.0001; }

Template Template Parameter

You can also pass templates to a template. In the example below we use template<class, class> to speciy that this is a template template parameters and then we proceed defining the template as usual.
template< template<class, class> class Container, class T, class Allocataor = std::allocator<T>> void Fun(const Container<T, Allocator>& c) { for (auto& e : c) { printf("%d\\n", e); } } std::vector<int> v {2, 3, 3}; Fun(v); std::list<char> li {'a', 'b'}; Fun(li);