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:

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