CS100 Lecture 26
Templates II
Contents
- Template specialization
- Variadic templates: an example
- Curiously Recurring Template Pattern (CRTP)
- Introduction to template metaprogramming
Template specialization
Templates are for generic programming, but some things need special treatments.
Specialization for a function template
template <typename T>
int compare(T const &lhs, T const &rhs) {
  if (lhs < rhs) return -1;
  else if (rhs < lhs) return 1;
  else return 0;
}
What happens for C-style strings?
const char *a = "hello", *b = "world";
auto x = compare(a, b);
This is comparing two pointers, instead of comparing the strings!
Specialization for a function template
template <typename T>
int compare(T const &lhs, T const &rhs) {
  if (lhs < rhs) return -1;
  else if (rhs < lhs) return 1;
  else return 0;
}
template <> // specialized version for T = const char *
int compare<const char *>(const char *const &lhs, const char *const &rhs) {
  return std::strcmp(lhs, rhs);
}
Write a specialized version of that function with the template parameters taking a certain group of values.
The type const T & with T = const char * is const char *const &: A reference bound to a const pointer which points to const char.
Specialization for a function template
It is also allowed to omit <const char *> following the name:
template <typename T>
int compare(T const &lhs, T const &rhs) {
  if (lhs < rhs) return -1;
  else if (rhs < lhs) return 1;
  else return 0;
}
template <>
int compare(const char *const &lhs, const char *const &rhs) {
  return std::strcmp(lhs, rhs);
}
Specialization for a function template
Is this a specialization?
template <typename T>
int compare(T const &lhs, T const &rhs);
template <typename T>
int compare(const std::vector<T> &lhs, const std::vector<T> &rhs);
No! These functions constitute overloading (allowed).
Specialization for a function template
Is this a specialization?
template <typename T>
int compare(T const &lhs, T const &rhs);
template <typename T>
int compare<std::vector<T>>(const std::vector<T> &lhs,
                            const std::vector<T> &rhs);
- Since we write int compare<std::vector<T>>(...), this is a specialization.
- 
However, such specialization is a partial specialization: The specialized function is still a function template. 
- 
Partial specialization for function templates is not allowed. 
Specialization for a class template
It is allowed to write a specialization for class templates.
template <typename T>
struct Dynarray { /* ... */ };
template <> // specialization for T = bool
struct Dynarray<bool> { /* ... */ };
Partial specialization is also allowed:
template <typename T, typename Alloc>
class vector { /* ... */ };
// specialization for T = bool, while Alloc remains a template parameter.
template <typename Alloc>
class vector<bool, Alloc> { /* ... */ };
Variadic templates: an example
A print function
template <typename First, typename... Rest>
void print(std::ostream &os, const First &first, const Rest &...rest) {
  os << first;
  if (/* `rest` is not empty */) // How to test this?
    print(os, rest...);
}
int i = 42; double d = 3.14; std::string s = "hello";
print(std::cout, s, d, i);
First, understand the different meanings of ....
- typename... Restindicates that- Restis a template parameter pack.
- const Rest &...restindicates that- restis a function parameter pack.
- rest...in- print(os, rest...)is pack expansion.
Compile-time recurrence
template <typename First, typename... Rest>
void print(std::ostream &os, const First &first, const Rest &...rest) {
  os << first;
  if (/* `rest` is not empty */) // How to test this?
    print(os, rest...);
}
std::string s = "hello"; double d = 3.14; int i = 42;
print(std::cout, s, d, i);
print(std::cout, s, d, i) leads to the instantiation of the following functions:
void print(std::ostream &os, const std::string &first, const double &rest0,
           const int &rest1);
void print(std::ostream &os, const double &first, const int &rest0);
void print(std::ostream &os, const int &first);
Note: first is not a parameter pack, so print must have at least two arguments.
sizeof...(pack)
How many arguments are there in a pack? Use the sizeof... operator, which is evaluated at compile-time.
template <typename First, typename... Rest>
void print(std::ostream &os, const First &first, const Rest &...rest) {
  os << first;
  if (sizeof...(Rest) > 0)
    print(os, rest...);
}
std::string s = "hello"; double d = 3.14; int i = 42;
print(std::cout, s, d, i);
Looks good ... But a compile-error?
sizeof...(pack)
Looks good ... But a compile-error?
b.cpp: In instantiation of ‘void print(std::ostream&, const First&, const Rest& ...)
[with First = int; Rest = {}; std::ostream = std::basic_ostream<char>]’:
b.cpp:11:8:   required from here
b.cpp:7:10: error: no matching function for call to ‘print(std::ostream&)’
    7 |     print(os, rest...);
It says that when First = int, Rest = {}, we are trying to call print(os) (with nothing to print).
Compile-time if
Let's see what the function looks like when Rest = {}:
template <typename First>
void print(std::ostream &os, const First &first) {
  os << first;
  if (false)   // sizeof... (Rest) == 0
    print(os); // Ooops! `print` needs at least two arguments!
}
The problem is that if is a run-time control flow statement! The statements must compile even if the condition is 100% false!
We need a compile-time if.
Compile-time if: if constexpr
if constexpr (condition)
  statement1
if constexpr (condition)
  statement1
else
  statement2
condition must be a compile-time constant.
Only when condition is true will statement1 be compiled.
Only when condition is false will statement2 be compiled.
Use if constexpr
template <typename First, typename... Rest>
void print(std::ostream &os, const First &first, const Rest &...rest) {
  os << first;
  if constexpr (sizeof...(Rest) > 0)
    print(os, rest...);
}
Solution without if constexpr: overloading.
template <typename T>
void print(std::ostream &os, const T &x) { os << x; }
template <typename First, typename... Rest>
void print(std::ostream &os, const First &first, const Rest &...rest) {
  print(os, first);
  print(os, rest...);
}
Curiously Recurring Template Pattern (CRTP)
Example 1: Uncopyable
We have seen this Uncopyable in Homework 6:
class Uncopyable {
  Uncopyable(const Uncopyable &) = delete;
  Uncopyable &operator=(const Uncopyable &) = delete;
public:
  Uncopyable() = default;
};
class ComplexDevice : public Uncopyable { /* ... */ };
A class can be made uncopyable by inheriting Uncopyable.
Example 1: Uncopyable
But if two classes inherit from Uncopyable publicly, odd things may happen ...
class Uncopyable {
  Uncopyable(const Uncopyable &) = delete;
  Uncopyable &operator=(const Uncopyable &) = delete;
public:
  Uncopyable() = default;
};
class Airplane : public Uncopyable {}; // Copying an airplane is too costly.
class MonaLisa : public Uncopyable {}; // An artwork is not copyable.
Uncopyable *foo1 = new Airplane();
Uncopyable *foo2 = new MonaLisa();
Ooops ... A Uncopyable* can point to two things that are totally unrelated to each other!
Example 1: Uncopyable
template <typename Derived>
class Uncopyable {
  Uncopyable(const Uncopyable &) = delete;
  Uncopyable &operator=(const Uncopyable &) = delete;
public:
  Uncopyable() = default;
};
class Airplane : public Uncopyable<Airplane> {};
class MonaLisa : public Uncopyable<MonaLisa> {};
Now Airplane and MonaLisa inherit from different bases: Uncopyable<Airplane> and Uncopyable<MonaLisa> are different types.
Example 2: Incrementable
template <typename T>
class Iterator {
  T *cur;
public:
  auto &operator++() {
    ++cur;
    return *this;
  }
  auto operator++(int) {
    auto tmp = *this;
    ++*this;
    return tmp;
  }
};
class Rational {
  int num;
  unsigned denom;
public:
  auto &operator++() {
    num += denom;
    return *this;
  }
  auto operator++(int) {
    auto tmp = *this;
    ++*this;
    return tmp;
  }
};
class AtomicCounter {
  int cnt;
  std::mutex m;
public:
  auto &operator++() {
    std::lock_guard l(m);
    ++cnt;
    return *this;
  }
  auto operator++(int) {
    auto tmp = *this;
    ++*this;
    return tmp;
  }
};
Example 2: Incrementable
With the prefix incrementation operator operator++ defined, the postfix version is always defined as follows:
auto operator++(int) {
  auto tmp = *this;
  ++*this;
  return tmp;
}
How can we avoid repeating ourselves?
Example 2: Incrementable
template <typename Derived>
class Incrementable {
public:
  auto operator++(int) {
    // Since we are sure that the dynamic type of `*this` is `Derived`,
    // we can use `static_cast` here to perform the downcasting.
    auto real_this = static_cast<Derived *>(this);
    auto tmp = *real_this;
    ++*real_this;
    return tmp;
  }
};
class A : public Incrementable<A> {
public:
  A &operator++() { /* ... */ }
  // The operator++(int) is inherited from Incrementable<A>.
};
Curiously Recurring Template Pattern
By writing the common parts of X, Y, Z, ... in a base class Base,
- we can avoid repeating ourselves.
- However, X,YandZhave a common base (which may lead to weird things), andBasedoes not know who is inheriting from it.
By letting X, Y, Z, ... inherit from Base<X>, Base<Y>, Base<Z>, ... respectively,
- each class inherits from a unique base class, and
- the base class knows what the derived class is, so a safe downcast can be performed.
CRTP idiom adopted in the standard library: std::enable_shared_from_this.
Introduction to template metaprogramming
Know whether two types are the same?
template <typename T, typename U>
struct is_same {
  static const bool result = false;
};
template <typename T> // specialization for U = T
struct is_same<T, T> {
  static const bool result = true;
};
- is_same<int, double>::resultis false.
- is_same<int, int>::resultis true.
- Are intandsignedthe same type? Letis_sametell you!
Know whether a type is a pointer?
template <typename T>
struct is_pointer {
  static const bool result = false;
};
template <typename T>
struct is_pointer<T *> { // specialization for <T *> for some T.
  static const bool result = true;
};
- is_pointer<int *>::resultis true.
- is_pointer<int>::resultis false.
- Is std::vector<int>::iteratoractually a pointer? Isint[10]the same thing asint *? Consult these "functions"!
<type_traits>
std::is_same, std::is_pointer, as well as a whole bunch of other "functions": Go to this standard library.
This is part of the metaprogramming library.
Compute \(n!\) in compile-time?
template <unsigned N>
struct Factorial {
  static const unsigned long long value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0u> {
  static const unsigned long long value = 1;
};
int main() {
  int a[Factorial<5>::value]; // 120, which is a compile-time constant.
}
Check whether an integer is a prime in compile-time?
template <unsigned N, unsigned Div> struct PrimeTest {
  static const bool result = (N % Div != 0) && PrimeTest<N, Div + 1>::result;
};
template <unsigned N> struct PrimeTest<N, N> { // end
  static const bool result = true;
};
template <unsigned N> struct IsPrime {
  static const bool result = PrimeTest<N, 2>::result;
};
template <> struct IsPrime<1u> {
  static const bool result = false;
};
static_assert(IsPrime<197>::result); // 197 is a prime
static_assert(!IsPrime<42>::result); // 42 is not
Seven basic quantities in physics
When performing computations in physics, the correctness in dimensions is important.
double mass = getMass();
double acceleration = getAcc();
double force = mass + acceleration; // Ooops! A mistake here!
Can we avoid such mistakes in compile-time? That is, to make mistakes in dimensions a compile error.
Seven basic quantities in physics
Each of the seven basic quantities corresponds to a template parameter:
template <int mass, int length, int time, int charge,
          int temperature, int intensity, int amount_of_substance>
struct quantity { /* ... */ };
using mass = quantity<1, 0, 0, 0, 0, 0, 0>;
using force = quantity<1, 1, -2, 0, 0, 0, 0>;
using pressure = quantity<1, -1, -2, 0, 0, 0, 0>;
using acceleration = quantity<0, 1, -2, 0, 0, 0, 0, 0>;
mass m = getMass();
acceleration a = getAcc();
force f = m + a; // Error! No match operator+ for 'mass' and 'acceleration'!
force f = m * a; // Correct.
If the arithmetic operations of different quantitys are defined correctly, we can avoid dimension mistakes in compile-time!
Template metaprogramming
Template metaprogramming is a very special and powerful technique that makes use of the compile-time computation of C++ compilers. (It is Turing-complete and pure functional programming.)
Learn a little bit more in recitations.
In modern C++, there are many more things that facilitate compile-time computations: constexpr, consteval, constinit, concept, requires, ...
Summary
Template specialization
- Specializes the template function / class when the template argument satisfies certain properties.
- Partial specialization: The specialization still has template parameters.
- Full specialization: The specialization no longer has no template parameters.
- Function templates cannot have partial specializations.
Summary
Variadic template example: A print function.
- pack...: pack expansion.
- sizeof...(pack)returns the number of arguments in a parameter pack. It is compile-time evaluated.
- if constexpr: compile-time- if: compile statements conditioned on a compile-time boolean expression.
Summary
Curiously Recurring Template Pattern (CRTP)
- Let Xinherit fromBase<X>.
- Each class inherits from a unique base class.
- The base class knows what the derived class is, so a safe downcast can be performed.
Template metaprogramming (TMP)
- TMP can shift work from runtime to compile-time, thus enabling earlier error detection and higher runtime performance.