模板是一个类或者一个函数,我们用一组类型或值对其进行参数化。
template
class Vector {
private:
T* elem;
int sz;
public:
explicit Vector(int s);
~Vector(){ delete[] elem;}
T& operator[](int i); //对于非常量的Vector
const T& operator[](int i) const; //对于常量Vector
int size() const {return sz;}
}
前缀template
指明T是该声明的形参。在引出类型参数时,使用class和使用typename是等价的,在旧式代码中经常出现将template
作为前缀。
成员函数的定义方式与之类似:
template
Vector::Vector(int s)
{
if(s < 0)
throw length_error{"Vector constructor: negative size"};
elem = new T[s];
sz = s;
}
template
const T& Vector::operator[](int i) const
{
if(i < 0 || size() <= i)
throw out_of_range{"Vector::operator[]"};
return elem[i];
}
可以用如下方式声明动态数组Vector:
Vector vc(200); // 200个字符组成的动态数组
Vector vs(17); // 17个字符串组成的动态数组
Vector> vli(45); // 45个整数链表组成的动态数组
为了让Vector支持范围for循环,需要为之定义适当的begin()和end()函数:
template
T* begin(Vector& x)
{
return &x[0]; // 指向第一个元素,或者指向末尾元素后面的一个位置
}
template
T* end(Vector& x)
{
return &x[0]+x.size(); // 指向末尾元素后面的一个位置
}
Vector的模板参数仅仅是一个typename还不够,还需要指定Element满足特定需求才能成为其元素:
template
class Vector {
private:
T* elem;
int sz;
// ...
};
这里,template
Element是一个谓词,用于检查T是否满足Vector需要的特性。
这种谓词叫做概念。在模板参数中指定一个概念,这叫作受限模板参数,拥有这种参数的模板叫作受限模板。
除了类型参数以外,模板还支持值作为参数。
template
struct Buffer{
constexpr int size() {return N;}
T elem[N];
// ...
};
值参数在很多环境中都有用。例如:Buffer允许我们创建任意尺寸的缓冲区而不需要使用动态内存分配:
Buffer glob; // 静态分配的全局char缓冲区
void fct()
{
Buffer buf; // 栈上分配的本地int缓冲区
// ...
}
但是,字符串字面量不可以作为模板值参数。但我们可以使用存放字符的数组来表示字符串:
template
void outs()
{
cout << s;
}
char arr[] = "Werid workaround";
void use()
{
outs <"straightforward use">(); // 到目前为止,这样不可行
outs(); // 诡异的间接解决方案
}
指定模板参数类型显得有些冗长,考虑使用标准库模板pair:
pair p = {1, 5.2};
// 等价于
pair p = {1, 5.2};
可以使用s后缀把字符串变成一个正确的string
template
class Vector {
public:
Vector(int);
Vector(initializer_list); // 初始化列表构造函数
// ...
};
Vector vs {"Hello", "World"}; // 可行:Vector
Vector vs1 {"Hello"s, "World"s}; // 可行:推导为Vector
Vector vs2 {"Hello"s, "World"}; // 错误:初始化列表中的数据类型不一致
模板还被广泛用于参数化标准库中的类型与算法。
要想表达将操作用类型或者值来参数化,有三种方法:
模板函数可以是成员函数,但不能是virtual函数。编译器不可能知道模板的所有实例,所以不可能像函数一样被调用。
一种特别有用的模板叫作函数对象(有时也被称为仿函数),它可以用来定义对象,该对象可以像函数一样被调用。
template
class Less_than{
const T val;
public:
Less_than(const T& v):val(v){}
bool operator()(const T& x) const {return x < val;} //函数调用操作符
};
名叫operator()的函数实现了应用操作符(),这个操作符又可被称作“函数调用操作符”“调用操作符”。
匿名函数表达式(lambda)
[&](int a){ return a < x; }
[&]是匿名函数的捕获列表,它指定了函数体内所有局部变量可以以引用的形式被访问。
析构函数提供了一种通用的方案,用于在作用域结束时隐式清除所有使用过的对象RAII,但如果需要进行的清理涉及多个对象,或者涉及不含析构函数的对象,可以定义一个finally()函数,它在作用域结束时执行:
void old_style(int n)
{
void *p = malloc(n* sizeof(int));
auto act = finally([&]{free(p);}); // 作用域结束时调用该匿名函数
}
实现finally()函数的方法:
template
[[nodiscard]] auto finally(F f)
{
return Final_action{f};
}
这里使用了[[nodiscard]]属性修饰,确保用户不会忘记保存所生成的返回值Final_action,因为正常完成功能必须保存它。
用来提供析构函数的类Final_action可以写成下面这样:
template
struct Final_action{
explicit Final_action(F f):act(f){}
~Final_action(){act();}
F act;
};