第15.2章 如何定义模板:函数、类、成员
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;
}
三个不那么显然的细节:
- 定义必须对实例化它的地方可见。这就是模板住在头文件里的原因,
也是本书从不把模板定义放进
.cpp的原因。编译器在调用点生成函数体, 所以它需要函数体,而不只是签名 (第6.7章)。 - 它是声明,所以不能是
static。static意味着内部链接,头文件里一个内部链接的模板会让每个翻译单元都各有一份所有实例化。 对在多个翻译单元实例化的函数模板来说,inline是隐含的;写出来没有坏处, 还能表明意图。 - 错误契约属于模板本身。这里的
noexcept声称每个 实例化都不会失败,对int以及任何<不会抛的类型都成立。 如果并非对所有实例化都成立,这个函数就是throws;若取决于类型, 就写成throws(condition)(第15.8章)。
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. 经验法则
- 模板定义放在头文件里、紧挨着声明。把它们分开就等于破坏实例化。
- 绝不给模板加
static;那会让每个翻译单元各有一份。 - 类外定义要写模板头,并且把类限定成
T<U>; 漏掉任何一个都是经典错误。 - 类模板的静态数据成员按实例化各定义一次,不带
static,写在头文件里。 - 只是给别的模板改名就用
using;需要成员或特化就用类模板。 - 先测量再动用
extern template:它挪动编译工作,并不消除它。