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):
| call | f(T x) | f(const T& x) | f(T&& x) |
|---|---|---|---|
f(n) | int | int | int& |
f(7) | int | int | int |
f(c) | int | int | const 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 returns4, not4.5. - Passing
argsinstead ofstd::forward<Args>(args). A temporary printscopiedwhere it should printmoved. - Bypassing the specializations inside the vector one. Writing
<<on an element instead of askingShow<T>prints1for a bool and an unquoted word. - Binding
bool&to astd::vector<bool>element.for (bool& b : v)does not compile, because elements are proxy objects; useconst 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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…