第15.2章 如何定义模板:函数、类、成员

English

1. 把语法讲一次,讲清楚

本章每一页都在某个东西前面写 template <...>。本页讲的正是那个“东西”: 模板定义长什么样,每个部分必须放在哪里,以及你真正会写的四种形状。

模板定义 = 一个前面带模板形参列表的声明,而它的函数体/成员可以使用那些形参:


template <template-parameter-list>
declaration-or-definition

列表里可以有类型形参(typename T)、值形参(::std::size_t N) 以及模板形参(template <typename...> typename container)。 外部作用域的其他规则都不变:模板可以在命名空间作用域、类里,或者在另一个模板里。

class T 与 typename T 在类型形参里含义完全相同; 该用 typename,因为 class 暗示那是个类,而 T 完全可能是 int。

2. 形状一:函数模板


template <typename T>
[[nodiscard]] inline T max_of_two(T a, T b) noexcept
{
	return a < b ? b : a;
}

三个不那么显然的细节:

max_of_two(3, 7) 与 max_of_two(2.5, 1.5) 分别实例化 max_of_two<int> 与 max_of_two<double>, 而调用语法里从没出现过形参。那就是推导 (第15.5章), 也是模板能被人用起来的原因。

3. 形状二:类模板及其成员

类模板的成员就是普通成员。在类内定义的成员隐式也是一个模板,可以直接用 T。在类外定义的成员要把模板头写两遍:一遍在定义前面, 一遍在限定名里 —— 那里类必须写成 box<T>。这是人们最容易写错的地方:


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_;
}
成员 写在哪 写法
类内定义的成员 类体内 普通成员,可随意使用 T
类外定义的成员 类之后 template <typename T> T box<T>::doubled()
静态数据成员 类之后 template <typename T> ::std::size_t counted<T>::live{};
构造/析构函数 两者皆可 规则与任何成员相同

注意静态成员定义里缺了什么:没有 static 关键字(它在类里), 也没有第二个 template <>。每个实例化 counted<int> 有自己的 live, counted<double> 有另一个。那不是全局变量,而是“每个实例化一个”的全局 —— 这正是模板的含义。

4. 形状三:默认实参与别名模板

模板形参可以有默认值,后面的形参可以使用前面的。fast_io 的容器靠这一点把分配器藏起来:


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>;

别名模板(using vector = ...)是一个只产生类型名的模板: 它没有独立身份,不能特化,也不能从调用里推导。给你不拥有的模板起一个更短或更具体的名字时用它; 需要成员、特化或推导时就用类模板。

5. 形状四:模板模板形参

模板形参本身也可以是一个模板。函数因此能说“任何装 T 的容器”, 而不点名是哪个容器:


template <template <typename...> typename container, typename T>
[[nodiscard]] inline ::std::size_t slots() noexcept
{
	return sizeof(container<T>);
}

container 是一个模板而不是类型,所以写成 container<T>。形参用 typename... 声明, 是为了接受带额外默认形参的容器 —— ::fast_io::vector 还带一个分配器, 依然适配。

6. 实例化发生在哪里,以及如何引导它

模板是隐式实例化的、按需发生的:第一次使用 box<::std::int_least32_t> 就在那个翻译单元里把整个东西编译出来。 若二十个翻译单元都用它,就会存在二十份机器码,链接器留下其中一份。 对一个很重的模板来说,这是大量重复编译。

两条语句把这份工作挪到一个地方:


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 是“别人会实例化它”的承诺;那个写着普通形式的 .cpp 就是那个“别人”。这与 第15.9章是同一场战斗,只是高了一层: 那边的问题是“每种类型一次实例化”,这里是“每个翻译单元一次实例化”。

7. 完整程序


#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);
	}
}

输出:


7
2.5
box: 21 doubled 42
live int=2 live double=1
after scope int=0
wrapper<::fast_io::vector, int> size=24

把中间两行放在一起读:两个 counted<int> 与一个 counted<double> 活着,每个实例化各算各的。最后一行是 ::fast_io::vector<int> 的大小 —— 在这台机器上是三个指针 —— 而整个过程里从没点名过那个容器。

8. 经验法则