C++ Primer第十六章
C++ Primer
模板与泛型编程
这一章会介绍一个神器-模板,我们可以用它实现泛型编程,我们先来说点大的官方的话:
- 面向对象编程能处理类型未知的情况,因为它都封装为对象了
- 泛型编程能处理在编译之前类型不知道的情况
说得这么玄乎,其实我们早就在用泛型编程了,比如我们学过的容器、迭代器和算法都是泛型编程,下面我们就来学习它,看看它到底方便在哪
模板是C++中泛型编程的基础,记住这句话:一个模板就是一个创建类或函数的蓝图
定义模板
我们现在要实现一个功能,很简单,比较两个值的大小,在实际中,我们可能需要写很多歌,因为这两个值可能是int,, double, string等等,如果我们写出这些函数就会发现,它们除了参数类型不同,其他都一样,相当于是重载函数,其实这样很烦,这样我们就引出了神器-用模板实现泛型编程(泛型,意思就是类型很泛,不再针对某一种类型):
//这是一个函数模板
template <typename T> //以关键字template开始,
//后面跟的是模板参数列表(不能为空),用逗号分割的一个或多个模板参数
int compare(const T &v1, const T &v2)
{
if(v1<v2){return -1;}
if(v2<v1){return 1;}
return 0;
}
这里我们写了一个函数蓝图,当我们实际去调用它时,编译器会根据实参来生成相应的函数:
compare(1, 0); //实例化出int compare(const int&, const int&)
compare(2.5, 3.8); //实例化出int compare(const double&, const double&)
compare("abc", "def"); //实例化出int compare(const string&, const string&)
怎么样,是不是很吊?
下面我们要看很多细节的东西,但泛型编程最大的好处我们已经了解了
模板类型参数
这个指的就是我们的T,这个T在后面调用编译的时候就被我们的实参类型替代了,它在模板中可以被用在任何地方:返回类型、函数参数类型、声明变量等,就跟变量类型一样用:
//都可以用
template <typename T>
T foo(T* p)
{
T tmp = *p;
return temp;
}
非类型模板参数
模板参数列表不一定非要放类型参数,还可以放非类型参数,只不过这个非类型参数是用来表示具体的一个值而不是一个类型
//处理字符串字面常量,我们要比较不同长度的,所以需要两个非类型参数
//当非类型参数在编译被替代时,一定是常量表达式,毕竟它是个值,不是类型
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
再强调一下:在模板定义内,模板非类型参数是一个常量,所以呢:
- 如果它是整型参数,必须是常量表达式
- 如果是指针或者引用,它所绑定的对象必须有静态生存期(谁也不知道你什么时候会调用编译生成,所以你得保证你绑定的东西不会被销毁)
inline和constexpr的函数模板
它俩还是可以修饰函数模板的,注意放置的位置,别忘了constexpr是编译时检查它修饰的是不是常量(我也是翻书看的。。。):
template <typename T>
inline T min(const T&, const T&);
模板编译
当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。
当我们在调用一个函数时,编译器只需要知道函数的声明就好了;当我们使用类类型对象时,也只需要类定义就好了(成员函数的定义不必已经出现),所以啊,我们可以把函数声明和类定义放在头文件中,把普通函数定义和类的成员函数的定义放在源文件
模板跟它们不一样的,为了生成一个实例化的版本,编译器需要掌握函数模板
类模板
前面我们用来介绍的模板泛型编程都是函数模板,看着还是挺简单的,接下来就来介绍一下更复杂的类模板。顾名思义,类模板是用来生成类的蓝图的,与函数模板不同,编译器不会为类模板推断模板参数类型,我们必须在模板名后的尖括号内提供额外信息,说了那么多,其实就是vector<int>
定义类模板
作为例子,我们将实现Blob,它不再只针对string,而是作为一个类模板,可以用于更多类型的元素:
//有些成员函数只写了声明,后面会再去实现定义的
template <typename T>
class Blob
{
public:
typedef T value_type;
typedef typename vector<T>::size_type size_type;
//构造函数
Blob();
Blob(initializer_list<T> il);
//Blob中的元素数目
size_type size() const {return data->size();}
bool empty() const {return data->empty();}
//添加删除元素
void push_back(const T &t) {data->push_back(t);}
void push_back(T &&t) {data->push_back(std::move(t));} //移动版本
void pop_back();
//元素访问
T& back();
T& operator[](size_type i);
private:
shared_ptr<vector<T>> data;
//检查错误,若i违法,抛出错误msg
void check(size_type i, const string &msg) const
};
除了类型变为T外,其他基本没改,是不是超级简单
实例化类模板
Blob<int> ia; //空Blob<int>
根据这个实例化的类,编译器会实例化出一个类,类中的T由int代替,总之你给什么类型,编译器就会实例化出对应的类,其实上面这段简单的代码包含了两个实例化过程:
- 类模板实例化为类
- 类实例化为对象
在模板作用域中引用模板类型
这句话读着是不是有些拗口啊,其实它的意思就是我们在Blob定义中用了vector(它本身也是模板),我觉得理解上不会有什么问题,没感觉到有啥奇怪,就不去解释了
类模板成员函数的定义
- 定义在类模板内部:内联函数
- 定义在外部:要写成函数模板的形式,其他规定与类成员函数一样
check和元素访问成员
我们首先来定义check成员:
template <tyoename T>
void Blob<T>::check(size_type i, const string &msg) const
{
if(i >= data->size())
{
throw std::out_of_range(msg);
}
}
接下来我们就可以实现元素访问成员函数了:
template <typename T>
T& Blob<T>::back()
{
check(0, "空了你还back");
return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
check(i, "这个索引不合法");
return (*data)[i];
}
template <typename T>
void Blob<T>::pop_back()
{
check(0, "空了你还弹出");
data->pop_back();
}
Blob构造函数
直接写了,没啥好说的,跟类模板成员函数定义差不多的:
//默认构造函数
template <typename T>
Bolb<T>::Blob() : data(make_shared<vector<T>>()){}
//接受参数的转换构造函数
template <typename T>
Blob<T>::Blob(initializer_list<T> il) : data(make_shared<vector<T>>(il)) {}
类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化
在类代码内简化模板类名的使用
我觉得没啥卵用:
template <typename T>
class BlobPtr
{
public:
BlobPtr& operator++(); //前置运算符,这里的BlobPtr等价于BlobPtr<T>
};
类模板和友元
原书写得很烦,我看了半天,其实蛮简单的,看代码就能知道
template <typename T>
class A
{
friend class Blob<T>; //相同类型的才是友元
template <typename X>
friend class B<X>; //B的所有实例全是友元
};
和内置类型做朋友
template <typename T>
class Bar
{
friend int; //int是这个模板类的朋友,能访问它所有实例的所有成员
};
顺便说一句模板也有类型别名,无非就是所谓的简化,也有static成员,概念跟原来一样
模板参数
唉,原书的作者太啰嗦了。。。
我这里就举几个我觉得有必要说明的
模板类型参数作为返回类型(在类模板外部定义的时候)
//在模板类外定义的时候,如何将模板类型参数作为返回类型
template <typename T>
typename T::value_type top(const T& c) //看懂了没,这个返回类型,就照着它写
{}
默认模板实参
就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参
我们先来给函数模板提供默认实参:
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{
if(f(v1, v2)){return -1;}
if(f(v2, v1)){return 1;}
return 0;
}
在这段代码中,我们为模板添加了一个类型参数F,表示可调用对象的类型,并定义了一个新函数参数f,绑定到一个可调用对象上
现在再来个类模板实参:
template <class T = int>
class Numbers
{
public:
Numbers(T v = 0) : val(v) {}
};
//因为有了默认实参,我们就可以这么干了
Numbers<> a;
成员模板
其实就是类里面的成员函数是模板函数,这个类可以是普通类也可以是类模板,下面就分别来讲
普通(非模板)类的成员模板
class Debugdel
{
public:
template <typename T>
void operator()(T *p) const //虽然是这个类的对象,但可以删除任何类型的指针
{
delete p;
}
};
类模板的成员模板
我们要为Blob类定义一个构造函数,接受俩迭代器,表示要拷贝的元素范围,由于我们希望支持不同类型序列的迭代器,因此要将这个构造函数定义为模板:
template <typename T>
class Blob
{
template <typename It>
Blob(It b, It e) : data(make_shared<vector<T>>(b, e)) {}
};
好的,下面我们来调用看看:
int ia[] = {0, 1, 2};
vector<long> v1 = {3, 4, 5};
//实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> a1(begin(ia), end(ia));