Ch15.2: Defining Templates: Functions, Classes, Members
1. The Syntax, Once and Properly
Every page of this chapter writes template <...> in front of
something. This page is about that something: what a template definition looks like,
where each part has to live, and the four shapes you will actually write.
A template definition is a declaration with a template parameter list in front of it, and a body that may use those parameters:
template <template-parameter-list>
declaration-or-definition
The list may contain type parameters (typename T), value parameters
(::std::size_t N) and template parameters
(template <typename...> typename container). Nothing else about the
enclosing scope changes: a template can be at namespace scope, in a class, or in
another template.
class T and typename T mean exactly the same thing in a type
parameter; typename is the spelling to use, because
class suggests a class and T may well be int.
2. Shape One: a Function Template
template <typename T>
[[nodiscard]] inline T max_of_two(T a, T b) noexcept
{
return a < b ? b : a;
}
Three details that are not obvious:
- The definition must be visible where it is instantiated. That is
why templates live in headers, and why this book never puts a template definition in
a
.cppfile. The compiler generates the body at the call site, so it needs the body, not just its signature (Ch6.7). - It is a declaration, so it must not be
static.staticmeans internal linkage, and a template with internal linkage in a header gives every translation unit its own copy of every instantiation. Theinlinekeyword is implicit for function templates instantiated in several translation units; writing it hurts nothing and documents intent. - The error contract belongs to the template. Here
noexceptclaims that every instantiation cannot fail, which is true forintand for anything whose<cannot throw. If that is not true for all of them, the function isthrows— or, when it depends on the type,throws(condition)(Ch15.8).
max_of_two(3, 7) and max_of_two(2.5, 1.5) instantiate
max_of_two<int> and max_of_two<double>, and the
call syntax never mentions the parameter. That is deduction
(Ch15.5), and it is the reason
templates are usable at all.
3. Shape Two: a Class Template, and Its Members
A class template's members are ordinary members. A member defined inside the class is
implicitly a template too, and may use T directly. A member defined
outside the class needs the full template header twice: once before the
definition, and once in the qualified name, where the class must be written as
box<T>. This is the part people get wrong:
template <typename T>
class box
{
T value_{};
public:
explicit box(T v) noexcept : value_{v} {}
[[nodiscard]] T const &get() const noexcept { return value_; }
[[nodiscard]] T doubled() const noexcept;
};
template <typename T>
[[nodiscard]] inline T box<T>::doubled() const noexcept
{
return value_ + value_;
}
| Element | Where | Written as |
|---|---|---|
| member defined inside | in the class body | plain member, uses T freely |
| member defined outside | after the class | template <typename T> T box<T>::doubled() |
| static data member | after the class | template <typename T> ::std::size_t counted<T>::live{}; |
| constructor / destructor | either | same rules as any member |
Note what is missing from the static member definition: no static keyword
(it is in the class) and no second template <>. Each instantiation
counted<int> gets its own live, and
counted<double> gets a different one. That is not a global; it is a
global per instantiation, which is exactly what a template means.
4. Shape Three: Default Arguments and Alias Templates
Template parameters may have defaults, and a later parameter may use an earlier one. fast_io's containers rely on this to keep the allocator out of the way:
template <typename T, typename tag = void>
struct wrapper
{
T value{};
};
template <typename T, typename Alloc = ::fast_io::native_global_allocator>
using vector = ::fast_io::containers::vector<T, Alloc>;
An alias template (using vector = ...) is a template that
produces a type name and nothing else: it has no separate identity, cannot be
specialised, and cannot be deduced from a call. Use it to give a shorter or more
specific name to a template you do not own; use a class template when you need members,
specialisation or deduction.
5. Shape Four: a Template-Template Parameter
A template parameter can itself be a template. It is how a function says
“any container of T” without naming the container:
template <template <typename...> typename container, typename T>
[[nodiscard]] inline ::std::size_t slots() noexcept
{
return sizeof(container<T>);
}
container is a template, not a type, so it is used as
container<T>. The parameter is declared with
typename... so that it accepts containers with extra defaulted parameters
— ::fast_io::vector takes an allocator, and still fits.
6. Where the Instantiation Happens, and How to Steer It
A template is instantiated implicitly, on demand: the first use with
box<::std::int_least32_t> compiles the whole thing in that
translation unit. If twenty translation units use it, twenty copies of the machine code
exist and the linker keeps one. For a heavy template that is a lot of duplicated
compilation.
Two statements move that work to one place:
extern template class box<::std::int_least32_t>; // in the header: do not instantiate here
template class box<::std::int_least32_t>; // in one .cpp: instantiate it here
extern template is a promise that someone else instantiates it; a
.cpp containing the plain form is that someone. This is the same fight as
Ch15.9, one level up: there the
problem was one instantiation per type, here it is one instantiation per translation
unit.
7. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <fast_io_dsal/vector.h>
// ---- a function template
template <typename T>
[[nodiscard]] inline T max_of_two(T a, T b) noexcept
{
return a < b ? b : a;
}
// ---- a class template: one member declared here, defined outside
template <typename T>
class box
{
T value_{};
public:
explicit box(T v) noexcept : value_{v} {}
[[nodiscard]] T const &get() const noexcept { return value_; }
[[nodiscard]] T doubled() const noexcept;
};
template <typename T>
[[nodiscard]] inline T box<T>::doubled() const noexcept
{
return value_ + value_;
}
// ---- a static data member of a class template
template <typename T>
class counted
{
public:
static ::std::size_t live;
counted() noexcept { ++live; }
~counted() noexcept { --live; }
};
template <typename T>
::std::size_t counted<T>::live{};
// ---- a default template argument
template <typename T, typename tag = void>
struct wrapper
{
T value{};
};
// ---- a template-template parameter
template <template <typename...> typename container, typename T>
[[nodiscard]] inline ::std::size_t slots() noexcept
{
return sizeof(container<T>);
}
// ---- explicit instantiation: emit this one here, once
template class box<::std::int_least32_t>;
int main()
{
try
{
::fast_io::io::println(max_of_two(3, 7));
::fast_io::io::println(max_of_two(2.5, 1.5));
box<::std::int_least32_t> b{21};
::fast_io::io::println("box: ", b.get(), " doubled ", b.doubled());
{
counted<int> c1{};
counted<int> c2{};
counted<double> c3{};
::fast_io::io::println("live int=", counted<int>::live, " live double=", counted<double>::live);
}
::fast_io::io::println("after scope int=", counted<int>::live);
wrapper<int> w{};
static_assert(::std::same_as<decltype(w), wrapper<int, void>>);
::fast_io::io::println("wrapper<::fast_io::vector, int> size=", slots<::fast_io::vector, int>());
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
7
2.5
box: 21 doubled 42
live int=2 live double=1
after scope int=0
wrapper<::fast_io::vector, int> size=24
Read the middle two lines together: two counted<int> objects and one
counted<double> are alive, and each instantiation counts separately.
The last line is the size of
::fast_io::vector<int> — three pointers on this machine
— obtained without ever naming the container.
8. Rules of Thumb
- A template definition goes in a header, next to the declaration. Splitting them breaks instantiation.
- Never mark a template
static; that gives each translation unit its own copy. - Write the template header on the out-of-class definition and qualify the
class as
T<U>; forgetting either is the classic error. - A static data member of a class template is defined once per instantiation, without
static, in a header. - Use
usingfor a template that only renames another; use a class template when you need members or specialisation. - Reach for
extern templateonly after measuring: it moves compile work, it does not remove it.