CTTCG 02 Class Templates
2023 年 5 月 7 日
定义位置
类模板只能在全局或者类声明中定义,不能在块或者函数中定义。
类模板外函数定义的写法
类模板中定义函数,T 类型参数可以省略;在类模板中声明,但在外提供函数定义,就需要在定义处给出完整的类型参数:
template<typename T> void Stack<T /* 不能省略 */>::push (T const& elem) { /* impl */ }
一个例外是析构函数和构造函数的名称处,因为这里不表类型,而是表示函数名。
template<typename T> void Stack<T>::Stack() { /* impl */ }
// ^^^^ 这里不能加参数T
P30 处理友元函数,比如向输出流打印。最简单的还是直接在类模板中提供定义:
friend std::ostream& operator<<(std::ostream& os, Stack const& s) {
for (auto const& i : s.elems) os << i << ' ';
return os;
}
如果只在类模板中写声明,要么 ① 把函数写成模板函数,并使用不同的类型参数符号,然后在类外补上定义:
template <class U> friend std::ostream& operator<<(std::ostream& os, Stack<U> const& s);
② 要么先声明类模板,再声明这个模板函数,然后在类中写:
friend std::ostream& operator<< <>(std::ostream& os, Stack const& s);
其中<>
表示函数需要匹配某个模板。最后在类外补上定义。
按需实例化
类模板只会实例化那些被用到的成员函数。这虽然降低了空间开销,但也使得一些问题很难被发现,比如常常在添加新的功能(调用之前没被调用的函数)后才发现旧代码无法编译的问题。
特化
可以用 template <>
特化类模板。若特化类模板要在外给出函数定义,只需要把他当成普通的类,不要再加 template <>
。
template<> class Stack<std::string> { /* impl */ };
void Stack<std::string>::push (std::string const& elem) /* impl */
偏特化
template<typename T>
class Stack<T*> { /* impl */ }
// ^^^ 注意这个尖括号是相对类模板的定义多出来的
偏特化任何模板时不能再提供默认模板参数,省略会导致继承默认参数,不省略则可以额外指定。特化函数模板时不能再提供函数默认参数值,但也不能省略参数声明。(一个是写在尖括号 <>
里的,一个是写在小括号 ()
里面的。)
偏特化是可以有更多的模板参数的,这样才能表达更加复杂和具体的结构,比如任意一个指向成员的指针至少需要两个模板参数来精确表达:
template<typename T, typename C>
class List<T* C::*> {
// 略
};
默认参数
类模板的默认参数应该放到靠后的位置,因为类模板是不能通过参数推导的。这个和模板函数注意对比。
Alias Templates
namespace std {
template<typename T> using add_const_t = typename add_const<T>::type;
}
alias templates 不能被特化。
C++ 17 省略类模板参数
Stack<int> intStack1;
Stack<int> intStack2 = intStack1;
Stack intStack3 = intStack1; // 能推断出类型参数是 int
由于推导的类型不一定是我们想要的,我们有时应该尽量在成员函数中写成接受值(而不是引用)的形式,从而使得数组能退化成首元素指针。P41
存在疑问时,类模板参数推导倾向于拷贝,而不是将参数作为子元素。P43
C++ 类模板在推导参数时不支持偏特化。就算是空参数也不行(空参数是 Java 的语法)。比如 Stack stk {"2"};
正确,而 Stack<> stk {"2"};
错误。
Deduction Guide
Stack(char const*) -> Stackstd::string;
对于聚合类模板也可以定义推导规则,比如:
template<typename T>
struct ValueWithComment {
T value;
std::string comment;
};
ValueWithComment(char const*, char const*)
-> ValueWithComment<std::string>;
这样 ValueWithComment vc2 = {"hello", "initial value"};
就会被推导规则引导。如果没有这条规则,代码就无法通过编译,因为推导是根据类模板构造函数的参数进行的,而聚合类没有构造函数。
注意,C++20 不需要 deduction guide 也可以编译上面的代码,应该是聚合类的定义、类模板参数推导要求有改动。
其他:
- 还能对推导规则添加 explicit 关键字。
- deduction guide 本身可以是模板:
template<typename T> S(T) -> S<T>;
- 和 auto 一样,当模板类参数被省略时,想要在同一语句中声明两个变量,必须保证他们的参数类型一致。
- deduction guide 本身只用于引导参数推导,不用于实际调用。因此对于多个引用类型(左引、右引)的构造函数,只需要写一个“值类型作为参数”的推导规则。