前面几篇我们了解了模版的经典用途:建立容器类。下面我们来看看模版另外的重要用途:描述一个或者一组程序接口的通用方式。
第一个例子
我们从一个计算数组中元素和的例子开始:
int sum(int* p, int size) {
int result = 0;
for(int i = 0; i < size; ++i) {
result += p[i];
}
return result;
}
主程序如下:
#include <iostream>
int main() {
int x[10];
for(int i = 0; i < 10; i++) {
x[i] = i;
}
std::cout << sum(x, 10) << std::endl;
}
sum函数需要知道数据是怎样存储的(因为要遍历),需要知道元素的类型,还需要知道对应类型的操作符operator+=
。我们看看能不能将这些特征从sum函数中拆分出来。
分离迭代方式
我们需要给sum函数一共一种遍历某个容器的方式,这个方式可以获取元素,通知什么时候遍历完成。我们已经有了这样的类:迭代器。所以我们可以先得到一个具体的迭代器,后面在考虑抽象的问题:
class Int_iterator
{
public:
Int_iterator() = default;
Int_iterator(int*, int);
~Int_iterator();
bool valid() const;
int next();
Int_iterator(const Int_iterator&);
Int_iterator& operator=(const Int_iterator&);
};
此时我们的sum函数可以写为:
int sum(Int_iterator ir) {
int result = 0;
while(ir.valid()) {
result += ir.next();
}
return result;
}
下面看看应该怎么实现Int_iterator类。
首先我们应该有一个成员用来保存外部的数组结构。另外还应该有个成员用来表示什么时候遍历完成了:我们可以采用还剩下多少个元素的方式,或者记录最后一个元素的地址的方式。
假设我们采用记录剩下元素多少的方式:
class Int_iterator
{
public:
Int_iterator():data(nullptr),length(0) {}
Int_iterator(int* x, int n):data(x),length(n) {}
~Int_iterator() {} // 因为不持有资源,不能释放掉data
bool valid() const { return length > 0; }
int next() { --length; return *data++; }
Int_iterator(const Int_iterator& other):data(other.data),length(other.length) {}
Int_iterator& operator=(const Int_iterator& other) {
if (other != *this) {
length = other.length;
data = other.data
}
return *this;
}
private:
int* data;
int length;
};
下面我们看看如何将Int_iterator这个类进行抽象化。
如果我们将Int_iterator类看作某个类模板的实例化类就很容易实现抽象化了。
template <class T>
class Iterator
{
public:
Iterator():data(nullptr),length(0) {}
Iterator(T* x, int n):data(x),length(n) {}
~Iterator() {} // 因为不持有资源,不能释放掉data
bool valid() const { return length > 0; }
T next() { --length; return *data++; }
Iterator(const Iterator<T>& other):data(other.data),length(other.length) {}
Iterator<T>& operator=(const Iterator& other) {
if (other != *this) {
length = other.length;
data = other.data
}
return *this;
}
private:
T* data;
int length;
};
此时我们只需要定义:typedefine Iterator<int> Int_iterator;
那么sum和主函数就都不需要进行修改了。
让我们重新来看看sum这个函数:
int sum(Int_iterator ir) {
int result = 0;
while(ir.valid()) {
result += ir.next();
}
return result;
}
这里我们可以将sum函数修改为函数模板,这样sum就变得更加通用了。
template <class T>
T sum(Iterator<T> ir) {
T result = 0;
while(ir.valid()) {
result += ir.next();
}
return result;
}
进行这样抽象以后sum就能用于其他类的对象数组中的元素和了。只有这个类满足下面的条件:
- 可以将0转换为该对象。
- 该类定义了
operator+=
。 - 该对象支持值语义(因为sum的返回值是值类型,不是引用)
将存储技术抽象化
目前为止,我们的sum函数只需要知道参数Iterator的类型就可以计算出数组中元素的和。但是如果存储的方式不是数组,而是链表,树等结构呢?因为我们只有一个Iterator类,所以根据我们的经验,可以容易想到通过继承来实现。定义一个Iterator的抽象类,然后让使用数组,链表,树结构的Iterator实现这个抽象类的接口。
template <class T>
class Iterator{
public:
virtual bool valid() const = 0;
virtual T next() = 0;
virtual ~Iterator() {} // 回忆一下,要被继承的类需要提供虚析构函数。
};
好,现在让我们来看看如何实现一个基于数组的Iterator:Array_iterator<T>
template <class T>
class Array_iterator : public Iterator<T> {
public:
Array_iterator():data(nullptr),length(0) {}
Array_iterator(T* x, int n):data(x),length(n) {}
~Array_iterator() {} // 因为不持有资源,不能释放掉data
bool valid() const { return length > 0; }
T next() { --length; return *data++; }
Array_iterator(const Array_iterator<T>& other):data(other.data),length(other.length) {}
Array_iterator<T>& operator=(const Array_iterator<T>& other) {
if (other != *this) {
length = other.length;
data = other.data
}
return *this;
}
private:
T* data;
int length;
};
然后再让我们看看sum应该怎么做。因为我们需要动态绑定生效的同时又不希望给带来用户操作指针的麻烦,所以我们采用引用的方式定义参数:
template <class T>
T sum(Iterator<T>& ir) { //这里是引用
T result = 0;
while(ir.valid()) { // 这里使用了动态绑定 1
result += ir.next(); // 这里又使用了动态绑定 2
}
return result;
}
#include <iostream>
int main()
{
int x[10];
for (int i = 0; i < 10; i++) {
x[i] = i;
}
Array_iterator<int> it(x, 10);
std::cout << sum(it) << std::endl; // 这里必须向sum中传递一个左值,如果传递一个右值会因为sum的参数不是const Iterator<T>&而报错。
}
通过上面的实现可以看出:
- 加和过程中每一次的判断和相加都是一次虚函数的调用,前面我们了解过虚函数的调用从内存引用上来看有很大的开销。
所以我们换一个思路。
我们采用组合的方式去除掉继承带来的问题,如下:
template <class T, class Iter>
void sum(T& result, Iter it)
{
result = 0;
while (it.valid()) {
result += it.next();
}
}
// 重写main
#include <iostream>
int main()
{
int x[10];
for (int i = 0; i < 10; i++) {
x[i] = i;
}
int r;
// 注意这里的Iterator<T>不是抽象类的那个
sum(r, Iterator<int>(x, 10)); // 这里第二个参数可以直接传一个右值
std::cout << r << std::endl;
}
总结
- 成功建立任何大系统的方式都是将它划分为各个独立的子模块。
- 重点在于这些子模块之间定义清晰的接口(也可以称为协议)
- 这里使用sum的例子阐明了如何使用c++的类定义用作接口。