Skip to content

Step 1 of 7 · Reading · ~3 min

Learn

Templates and STL Algorithms

Templates Beyond the Basics

After this lesson you can predict what the compiler deduces for T, override it, forward arguments through a template without copying them, and give a class template different behaviour for chosen types.

In Fundamentals you wrote function and class templates and let the compiler pick T. Here you learn how it picks, and how to steer and specialise it.

How T is deduced

Each parameter votes for T on its own, and the votes must agree:

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

larger(3, 4);      // 4
larger(3, 4.5);    // error: candidate template ignored: deduced
                   //   conflicting types for parameter 'T' ('int' vs. 'double')

Fixes: name T, as in larger<double>(3, 4.5) (gives 4.5); or use two type parameters and return typename std::common_type<A, B>::type from <type_traits>.

The parameter's shape decides what T captures (int n, const int c):

callf(T x)f(const T& x)f(T&& x)
f(n)intintint&
f(7)intintint
f(c)intintconst int&

By value drops const and references. A T&& on a deduced T is a forwarding reference: it binds to anything and remembers whether the argument was an lvalue or an rvalue.

Forwarding and parameter packs

A parameter pack (typename... Args) stands for zero or more types, and args... expands to the matching arguments; that is how make_unique<T>(a, b, c) takes any number of them. A named parameter is always an lvalue, even when its type is T&&, so passing it on loses the rvalue. std::forward<Args>(args) restores it:

struct Note {
    Note(const std::string&) { std::cout << "copied\n"; }
    Note(std::string&&)      { std::cout << "moved\n"; }
};
template <typename T, typename... Args>
T build_plain(Args&&... args) { return T(args...); }
template <typename T, typename... Args>
T build(Args&&... args) { return T(std::forward<Args>(args)...); }

std::string s = "x";
build_plain<Note>(std::string("tmp"));   // copied   (rvalue lost)
build<Note>(std::string("tmp"));         // moved
build<Note>(s);                          // copied   (s lives on)

To walk a pack, a variadic overload handles the head and calls itself with rest..., while a one-element overload ends the recursion.

A template parameter can also be a value. Taking an array by reference deduces its length, which pointer decay threw away: template <typename T, std::size_t N> std::size_t length(T (&)[N]) { return N; } returns 4 for int days[4], and an int* does not compile (could not match 'T [N]' against 'int *').

Specialization

The primary template is the fallback. A full specialization (template <>) replaces it for one exact type; a partial specialization keeps some parameters open and matches a shape. The most specialised match wins.

template <typename T> struct TypeName { static std::string get() { return "other"; } };
template <> struct TypeName<int> { static std::string get() { return "int"; } };
template <typename T> struct TypeName<T*> { static std::string get() { return TypeName<T>::get() + "*"; } };
// double -> other   int -> int   int* -> int*   int** -> int**   char* -> other*

TypeName<T*> matches any pointer, peels one *, and asks TypeName<T> about the rest. Function templates cannot be partially specialised; use overloads. Before C++20 concepts, constrain a template with static_assert(std::is_integral<T>::value, "needs an integer") so the error shows your message.

Common mistakes

  • Casting to silence a mixed-type error. larger(3, int(4.5)) compiles and returns 4, not 4.5.
  • Passing args instead of std::forward<Args>(args). A temporary prints copied where it should print moved.
  • Bypassing the specializations inside the vector one. Writing << on an element instead of asking Show<T> prints 1 for a bool and an unquoted word.
  • Binding bool& to a std::vector<bool> element. for (bool& b : v) does not compile, because elements are proxy objects; use const bool&.

Your exercise

Finish Show<T>, a class with one static function str that turns a value into text: a full specialization for bool and one for std::string, and a partial specialization for std::vector<T> whose elements go through Show<T>. The starter reads the input and prints through Show. The likeliest bug is printing an element without going through Show.

Up nextSTL Algorithms in Depth: Captures, Reshaping and Sorted SearchesTemplates and STL Algorithms

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…