← C++, End to End

C++, End to End

Templates and Generic Classes

An earlier chapter covered function templates. Class templates apply the same idea to whole classes: write the logic once, parameterized by type, and let the compiler generate a concrete version for each type you actually use.

Class templates

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Box<int> and Box<std::string> are two entirely different, independently compiled classes as far as the compiler is concerned, generated from the same template. This is why templates are described as compile-time polymorphism: there's no runtime dispatch overhead like virtual functions have, because there's no ambiguity left to resolve by the time the program runs, every use site already knows exactly which concrete type it's dealing with.

Template specialization

Sometimes the generic implementation is wrong (or just suboptimal) for a specific type. Full specialization lets you provide a completely separate implementation for one exact type:

c++ · live, editable, runnableOpen in Compiler Explorer ↗

Partial specialization does the same thing but for a subset of a multi-parameter template (for example, specializing a Pair<T, U> template just for when U is a pointer type), rather than one exact concrete type.

Variadic templates and parameter packs

A variadic template accepts any number of template arguments, using a parameter pack (Args...). This is how things like std::make_unique and std::tuple are implemented under the hood.

c++ · live, editable, runnableOpen in Compiler Explorer ↗

That ((std::cout << args << " "), ...) is a fold expression (C++17): it expands the parameter pack, applying the comma operator across every argument, which is a compact way to "do this for every argument in the pack" without writing recursive template unpacking by hand, which is how this was done before C++17 and was considerably uglier.

Concepts: constraints that actually explain themselves

Before C++20, constraining a template to only accept certain kinds of types relied on SFINAE (substitution failure is not an error), a genuinely clever but genuinely unreadable technique involving std::enable_if and template metaprogramming tricks. If you got it wrong, the compiler error was often a wall of template instantiation backtraces with no clear indication of what you actually did wrong.

Concepts (C++20) replace most of that with a named, readable constraint you can attach directly to a template parameter:

c++ · live, editable, runnableOpen in Compiler Explorer ↗

If you're writing a template that only makes sense for certain kinds of types, use a concept to say so directly, rather than letting a misuse fail deep inside the implementation with a confusing error. Concepts are a genuinely large usability improvement, and by now the reasonable default whenever you'd have reached for SFINAE in older code.

Try it yourself: write a constrained template

Write a template function clamp(value, low, high) constrained to only accept types satisfying std::totally_ordered (a standard concept meaning the type supports all six comparison operators), then try calling it with a type that doesn't satisfy it and read the resulting error.

c++ · live, editable, runnableOpen in Compiler Explorer ↗