C++ Templates(3):基本技术
1. typename
关键字 $typename$ 用于标识模版内部的某个成员是一个类型。
template<typename T>
class MyClass {
public:
void foo() {
typename T::SubType* ptr;
}
};
这里的第二个 $typename$ 就是用于标识 $SubType$ 是一个类型。如果没有 $typename$ ,$SubType$ 就必须是一个非类型成员,从而表达式会变成乘法运算,这会导致一个错误。
$typename$ 的一个应用是在泛型类中声明迭代器:
#include <iostream>
template<typename T>
void printcoll(T const& coll) {
typename T::const_iterator pos;
typename T::const_iterator end(coll.end());
for (pos = coll.begin(); pos != end; ++pos) {
std::cout << *pos << ' ';
}
std::cout << '\n';
}
2. 零初始化
对于基本类型,例如 $int$ 、$double$ 或指针类型,它们没有默认构造函数,意味着初始化时必须被赋予一个初始值,否则它们的值就是不确定的。假设你有一个模版变量,并且想要让它拥有初始值,但是内置类型并不会初始化。出于这个原因,你可以显式初始化,这会让它们的值变为 $0$ :
template<typename T>
void foo { T x{}; }
这种初始化称为值初始化,表示要么调用一个已提供的构造函数,要么进行零初始化。这种方式甚至对 $explicit$ 构造函数也生效。
C++11
之前,使用零初始化的方式是小括号:
T x = T();
在C++17
之前,只有拷贝初始化的构造函数不是 $explicit$ 时才能使用这个机制,C++17
强制性的拷贝省略避开了这种限制。如果类没有默认构造函数,那么也可以通过花括号调用初始化列表构造函数。
3. 原始数组和字符串常量模版
当给模版参数传递原始数组和字符串常量时,有些问题要注意。首先,如果模版参数声明为引用,那么推导出来的参数是未退化的。只有当模版参数是值传递时,才会发生类型退化,这时原始数组和字符串常量都会退化为指针。
template<typename T, int N, int M>
bool less(T (&a)[N], T (&b)[M]) {
for (int i = 0; i < N && i < M; i++) {
if (a[i] < b[i]) return true;
if (b[i] < a[i]) return false;
}
return N < M;
}
std::cout << less("ab", "abc") << std::endl;
在上面的例子中,$T$ 是 $char$ $const$ ,$N$ 和 $M$ 分别是 $3$ 和 $4$ 。
如果你只是想要为字符串常量提供一个模版,那么你可以这样:
template<int N, int M>
bool less(char const (&a)[N], char const (&b)[M]) {
for (int i = 0; i < N && i < M; i++) {
if (a[i] < b[i]) return true;
if (b[i] < a[i]) return false;
}
return N < M;
}
我们还可以对模版进行部分特例化。
#include <iostream>
template<typename T>
struct MyClass;
template<typename T, std::size_t SZ>
struct MyClass<T[SZ]> {
static void print() {
std::cout << "print() for T[" << SZ << "]\n";
}
};
template<typename T, std::size_t SZ>
struct MyClass<T(&)[SZ]> {
static void print() {
std::cout << "print() for T(&)[" << SZ << "]\n";
}
};
template<typename T>
struct MyClass<T[]> {
static void print() {
std::cout << "print() for T[]\n";
}
};
template<typename T>
struct MyClass<T(&)[]> {
static void print() {
std::cout << "print() for T(&)[]\n";
}
};
template<typename T>
struct MyClass<T*> {
static void print() {
std::cout << "print() for T*\n";
}
};
template<typename T1, typename T2, typename T3>
void foo(int a1[7], int a2[], // 指针
int (&a3)[42], // 已知长度数组引用
int (&x0)[], // 未知长度数组引用
T1 x1, // 退化后的类型
T2& x2, T3&& x3) { // 引用
MyClass<decltype(a1)>::print(); // MyClass<T*>
MyClass<decltype(a2)>::print(); // MyClass<T*>
MyClass<decltype(a3)>::print(); // MyClass<T(&)[SZ]>
MyClass<decltype(x0)>::print(); // MyClass<T(&)[]>
MyClass<decltype(x1)>::print(); // MyClass<T*>
MyClass<decltype(x2)>::print(); // MyClass<T(&)[]>
MyClass<decltype(x3)>::print(); // MyClass<T(&)[]>
}
int main() {
int a[42];
MyClass<decltype(a)>::print(); // MyClass<T[SZ]>
extern int x[];
MyClass<decltype(x)>::print(); // MyClass<T[]>
foo(a, a, a, x, x, x, x);
}
int x[] = {0, 8, 15};
4. 成员模版
类成员也能成为模版,可以在内部类和成员函数中声明。
template<typename T>
class Stack {
public:
template<typename T2>
Stack& operator=(Stack<T2> const&);
// ...
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& op2) {
Stack<T2> tmp(op2);
elems.clear();
while(!tmp.empty()) {
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
Stack<int> intStack;
Stack<float> floatStack;
floatStack = intStack;
你也可以通过友元简化上面的代码:
template<typename T>
class Stack {
public:
template<typename T2>
Stack& operator=(Stack<T2> const&);
template<typename> friend class Stack;
// ...
};
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& op2) {
elems.clear();
elmes.insert(elems.begin(),
op2.elems.begin(),
op2.elems.end());
return *this;
}
成员函数模版也可以被部分特例化或完全特例化。
class BoolString {
private:
std::string value;
public:
BoolString(std::string const& s) : value(s) {}
template<typename T = std::string>
T get() const { return value; }
};
template<>
inline bool BoolString::get<bool>() const {
return value == "true" || value == "1" || value == "on";
}
注意你不需要也不能声明特例化函数,你只能定义它们。因为它是一个完全特例化并且在头文件里,你需要声明为 $inline$ 来避免当定义被不同的翻译单元包含时产生的错误。
模版成员函数可以在任何允许拷贝或者移动的对象中使用。类似于上面定义的赋值运算符,它们也能作用于构造函数。然而,注意模版构造函数或者模版赋值运算符不会代替预定义的构造函数或者赋值运算符。成员模版不会被视为特殊成员函数,这意味着对于相同的类型的赋值,还是会调用默认的拷贝赋值运算符。这既是一件好事也是一件坏事:
- 当模版构造函数或者赋值运算符比起预定义的构造函数或者赋值运算符是更优匹配时,就算这个模版函数是为了其他某个类型而存在,它们也会被调用;
- 完全模版化拷贝或者移动构造函数是很困难的。
有时候,为了调用指定模版参数版本的成员函数,我们可以使用 $template$ 关键字,它可以确保 < 是作为模版参数列表的开始而使用的。
template<unsigned long N>
void printBitset(std::bitset<N> const& bs) {
std::cout << bs.template to_string<char, std::char_traits<char>,
std::allocator<char>>();
}
在上面的例子中,如果没有 $template$ 关键字,编译器会认为 < 是一个小于号。注意这个问题只有在 $.template$ 之前的类型构造依赖于模版参数时才会出现。在我们的例子中,$bs$ 依赖于模版参数 $N$ 。$.template$ ( 类似的还有 ->$template$ 和 $::template$ ) 应该只在模版中且它们之后某些调用依赖于模版参数时使用。
C++14
中的lambda
表达式也可以使用泛型。
[](auto x, auto y) { return x + y; }
它等价于以下类:
class SomeCompilerSpecificName {
public:
SomeCompilerSpecificName();
template<typename T1, typename T2>
auto operator()(T1 x, T2 y) const {
return x + y;
}
};
5. 变量模版
从C++14
开始,变量也能被模版化,称为变量模版。
template<typename T>
constexpr T pi{3.141592653589793285};
std::cout << pi<double> << '\n';
std::cout << pi<float> << '\n';
你也可以在不同的翻译单元声明:
/* header.hpp */
template<typename T> T val{}; // 零初始化
/* translation unit 1 */
#include "header.hpp"
int main() {
val<long> = 42;
print();
}
/* translation unit 2 */
#include "header.hpp"
void print() {
std::cout << val<long> << '\n'; // print 42
}
还可以指定一个默认模版参数:
template<typename T = long double>
constexpr T pi = T{3.1415926535897932385};
std::cout << pi<> << '\n';
要注意你必须使用尖括号,否则会报错:
std::cout << pi << '\n'; // 错误
变量模版也可以使用非类模版参数:
#include <iostream>
#include <array>
template<int N>
std::array<int, N> arr{};
template<auto N>
constexpr decltype(N) dval = N;
int main() {
std::cout << dval<'c'> << '\n';
arr<10>[0] = 42;
for (std::size_t i = 0; i < arr<10>.size(); i++) {
std::cout << arr<10>[i] << '\n';
}
}
变量模版的一个有用的应用就是定义代表类成员的变量。
template<typename T>
class MyClass {
public:
static constexpr int max = 1000;
};
template<typename T>
int myMax = MyClass<T>::max;
auto i = myMax<std::string>;
从C++17
开始,标准库使用变量模版来定义一些产生布尔值的 $type$ $traits$ 模版的缩写,比如:
namespace std {
template<typename T>
constexpr bool is_const_v = is_const<T>::value;
}
6. 模版模版参数
一个类模版可以接收一个模版作为参数。
template<typename T,
template<typename Elem> class Cont = std::deque>
class Stack {
private:
Cont<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const { return elems.empty(); }
};
这样 $deque$ 的实例化类型就由第一个模版参数 $T$ 决定了。像这样使用第一个参数来实例化第二个参数还是挺独特的。一般来讲,你可以使用任何参数来实例化模版模版参数。
通常,除了使用 $typename$ 关键字外,你也可以使用 $class$ 关键字。在C++11
之前,$Cont$ 只能使用 $class$ 关键字定义:
template<typename T,
template<class Elem> class Cont = std::deque>
class Stack;
从C++11
开始,我们也可以使用别名模版代替 $Cont$ ,但是直到C++17
,我们才能使用 $typename$ 关键字来声明模版模版参数:
template<typename T,
template<typename Elem> typename Cont = std::deque>
class Stack;
因为模版模版参数的模版参数名没有被用到,所以我们也可以省略:
template<typename T,
template<typename> class Cont = std::deque>
class Stack;
如果你尝试使用新版本的 $Stack$ ,你可能会发现编译出错了,出错信息是默认的 $std::deque$ 与模版模版参数 $Cont$ 不匹配。这个问题是因为在C++17
之前,模版模版参数的模版参数必须精确匹配代替它的模版类型的模版参数。这种情况下,默认的模版参数是不会被考虑的。所以,我们要修改程序:
template<typename T,
template<typename Elem,
typename Alloc = std::allocator<Elem>>
class Cont = std::deque>
class Stack;
这样声明的问题就是并非所有标准库容器都可以使用这个模版类了,比如 $std::array$ ,因为它需要提供的是一个数组长度值而不是一个分配器。