第十六章(模板和泛型编程)
1).泛型编程。例如,容器,迭代器,算法。提供一些信息进行实例化。可以在编译时就知道类型。
- 模板时泛型编程的基础。
2).OOP,动态绑定,可以在运行时才知道类型。
3).以上两者都可以处理编写程序时,不知道类型的情况。
/1.定义模板
1).比较所有类型的数据大小,
- 需要定义很多的重载函数,(我们所能想到的)
- 而且如果是用户自定义的类型,这种策略就不可行。
2).比较函数,除了数据类型,函数体,函数名,参数名,返回值类型完全一样。
//1.函数模板
1).定义一个函数模板,而不是定义很多的重载函数。
2).template<typename/class T>
(模板参数列表,里面就是模板参数分为类型参数和非类型参数,非类型参数需要是常量表达式,注意每一个类型参数前 都需要 typename/class
(类类型,但其实不只是类类型);或者非类型参数前使用unsigned
,
- (可以交叉使用)且它们以逗号隔开)
- (一旦有了上述定义,T可以看成类型说明符。和内置数据类型,类类型没有什么两样。可以定义变量等。需要用的时候就把它看成是类型名称即可。)
- 然后书写正常的完整函数即可。这样就完成了一个函数模板。
{
template<typename T>
int compare(const T &v1,const T &v2) {
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;
}
}
3).注意事项。
- 模板定义中,模板参数列表不能为空
- 将模板参数列表和函数的参数列表相比较。(我们可以隐式或者显式地执行模板实参)注意使用时(和普通函数一样地调用)是编译器通过识别我们的函数实参,(推断出模板的实参,绑定到模板参数中)判断出类型然后将T用实际的类型代入,这就是实例化的过程。
{
vector<int> vec1{1,2,3,4};
vector<int> vec2{2,3,4};
compare(vec1,vec2);
// 实例化出,int compare(const vector<int> &v1, const vector<int> &v2);
}
4).当编译器使用模板实参代替对应的模板参数创建出模板的一个实例。(就是实例化一个模板)
5).非类型参数的典型就是数组的引用。比较字符串。因为我们不知道大小。我们需要定义数组的引用。
6).注意非类型参数可以是以下类型。
- 整型,
- 指针,
- 引用。
- 绑定到非类型整型参数的实参必须是一个常量表达式。而对于指针和引用要求是,实参必须具有静态的生存期(
static
变量)。当然指针可以用nullptr
或者0的常量表达式来实例化。
7).由于非类型参数是一个常量,所以我们可以,在需要使用常量的地方就可以使用它们。例如,指定数组的大小。
8).例如,比较两个字符串数组。就是两个const char *
{
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1,p2);
}
compare("hi", "mom");
// 将会实例化出以下的形式
int compare(const char (&p1)[3], const char (&p2)[4]);
// 需要注意的是,c风格字符串是以'\0'结束。
}
9).函数模板也可以是inline
或者constexpr
的。注意声明位置。
{
template <...>
inline int ...;
template <...>
constexpr int ...;
}
10).模板程序需要尽量减少对类型的依赖。
- 设为
const
的引用。避免一些类无法进行拷贝。 - 使用
less<T>
来实例化,而不是使用<>
。可以在函数体里面使用类型模板。
{
// 先构建一个空对象,再传入参数进行使用。
if (less<T>()(v1,v2)) return -1;
if (less<T>()(v2,v1)) return 1;
}
11).关于模板的定义和函数,类的区别。
- 当编译器运到一个模板定义时,它并不生成代码。只有当我们实例化出一个特定版本时,编译器才会生成代码。
- 对于函数,我们调用时,编译器只需要掌握声明,也就是我们需要先声明再使用;
- 对于一个类类型的对象,我们使用时,类的定义必须是可用的;但是成员函数的定义不必已经出现。
- 因此,函数的声明和类的定义放在头文件;普通函数的定义和类的成员函数的定义
放在源文件。 - 而对于模板,为了实例化一个版本,编译器需要掌握函数模板,或者类模板成员函数的定义。因此,这些函数的定义需要放在头文件中。即保证函数模板实例化时,函数的定义是可见。
- 其他就是,模板设计者,保证用在模板中的可见的类型都是可见的,例如,类类型是已经定义好的等。这一点,用户和设计者的要求一致。
12).编译错误的报告(包含三个阶段)。
- 第一个阶段,编译模板本身。一般只是检查语法错误;例如变量名字,分号等。
- 第二个阶段,遇到模板的使用时,参数的数目,类型是否匹配。
- 第三个阶段,模板的实例化,会发现类型相关的错误。依赖于编译器如何管理实例化,这类错误又可能在链接时才报告。
13).例子。
- 例如,我们在使用
compare
的<>
进行比较的版本的时候,编译器只有当进行第三个阶段时,才会知道错误。
练习,
- 16.4,编写
find
算法。
{
// 当传入的是迭代器,也会按照迭代器的类型进行解析。
// 例如,vector<int>::iterator
// 或者,list<string>::iterator
template <typename I, class T>
I find(I b, I e, const T &v) {
while (b != e && *b != v) {
++b;
}
return b;
}
}
- 16.5,打印任意大小的数组。
{
template <typename T, size_t N>
void print(const T (&a)[N]) {
for (auto iter = begin(a); iter != end(a); ++iter) {
cout << *iter << " ";
}
}
}
- 16.6,设计
begin
和end
{
template <typename T, size_t N>
const T* my_begin(const T (&a)[N]) {
return &(a[0]);
}
template <typename T, size_t N>
const T* my_end(const T (&a)[N]) {
return (&(a[0])) + N;
}
}
- 16.7,设计
constexpr
函数模板,返回的是一个给定的数组的大小。利用函数模板直接可以给出。
{
template <typename T, size_t N>
constexpr size_t arr_size(const T (&a)[N]) {
return N;
}
}
//2.类模板
1).与函数模板不一样的是,它不能用推断的方式来得到模板参数。和我们以前使用模板一样,我们都需要显式地在尖括号中加入额外的信息————模板实参列表(显式模板实参。),来代替模板参数。
2).定义类模板Blob
。
{
template <typename T>
class Blob {
public:
typedef T value_type;
// 为什么需要加上typename
typedef typename vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(initializer_list<T> il);
// Blob中的元素数目
size_type size() const {
return data->size();
}
bool enpty() 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;
void check(size_type i, const string &msg) const;
};
//实例化类模板
Blob<int> ia;//空的Blob
Blob<int> ia2 = {0,1,2,3,4};//有5个元素的Blob
// 此时编译器会实例化出一个与下面定义等价的类。
template<>
class Blob<int> {
...//所有的T都使用int代替。
};
}
3).类模板不是一个类型名字。
4).定义在模板类之外的成员函数,也应该以模板的形式。
- 和类模板一样的模板参数。
- 类的名字必须包含模板实参,当我们定义成员函数时,模板实参和模板参数一致。
{
// check成员
template<typename T>
void Blob<T>::check(size_t i, const string &smg) {
if (i >= data->size()) {
throw out_of_range(msg);
}
}
// 定义back。
template<typename T>
T& Blob<T>::back() {
check(0, "back on empty Blob");
return data->back();
}
//定义下标运算
template<typename T>
T& Blob<T>::operator[](size_type t) {
check(t, "out of range");
return (*data)[t];
}
// 定义pop_back函数
template<class T>
void Blob<int>::pop_back() {
check(0, "pop_back on empty Blob");
data->pop_back();
}
// Blob的构造函数
template<class T>
Blob<T>::Blob() : data(make_shared<vector<T>>()) {}
// 接受一个initializer_list的构造函数
template<class T>
Blob<T>::Blob(initializer_list<T> il) : data(make_shared<vector<T>>(il)) {}
// 使用。
Blob<string> articles = {"1","2"};
// 使用的是initializer_list<string>的构造函数。
// 此时会将初始化列表隐式地转化位string。
}
5).模板类的成员函数,只有当被使用时,才会别实例化。因此即便是有一些类型不完全符合模板的操作的要求。但是我们依然可以用该类型来实例化类。
6).当我们使用一个类模板时必须提供模板实参,但是有一个例外。在类模板自己的作用域中时,我们可以直接使用模板名字,而不用提供实参。这样就会默认是我们的类型参数。
{
// 例如函数的返回值类型(在模板类BlobPtr的类中)
BlobPtr& operator++();
BlobPtr& operator--();
// 编译器会这样处理
BlobPtr<T>& operator++();
BlobPtr<T>& operator--();
}
7).在类外时,必须注意,当遇到类名字之后才表示进入类的作用域。
{
template<typename T>
BlobPtr<T> BlobPtr<T>::operator++() {
BlobPtr ret = *this;//保存当前值
++*this;//我们自己定义的++会进行检查。
return ret;
}
}
8).类模板和友元
- 一个类模板中包含一个非模板友元。则友元被授权可以访问所有模板实例。
- 如果友元是模板,类可以授权给所有友元实例,也可以授权给部分实例。
9).例子。普通类的友元各种状态。
{
// 前置声明,可以不具有类型参数的名称。
// 但是一定包括类型参数模板。
template <typename T> class BlobPtr;
// operator==中的参数需要。
template <typename T> class Blob;
template <typename T>
bool operator==(const Blob<T> &, const Blob<T> &);
// 定义一个模板类。
template<typename T>
class Blob {
//每一个Blob的实例将访问权赋予用相同类型实例化的BlobPtr和相等运算符。
// 因为这里使用的是BlobPtr的模板参数。
friend class BlobPtr<T>;
friend bool operator==(const Blob<T> &,const Blob<T> &);
};
}
10).模板类的友元的各中状态。
{
template<typename T> class Pal;
class C {//C是一个普通的类
friend class Pal<C>;//用C实例化的Pal是一个友元
template <typename T> friend class Pal2;//Pal2的所有实例都是C的友元。无需前置声明。
};
template <typename T> class C2 {
// C2的每一个实例化都将和相同实例化的Pal声明为友元。
friend class Pal<T>;
// Pal2的每一个实例化,都是C2的每一个实例的友元,不需要前置声明。
// 注意这里的类型名称是X。
template <typename X> friend class Pal2;
// Pal3不是模板类,所以它是C2所有实例的友元。
friend class Pal3;//不需要前置声明。
};
}
11).令模板自己的类型参数成为友元。
{
template <typename T> class Bar {
friend T;//T可能是类类型,例如Sales_data
// 可能是一个函数,Foo
// 或者是内置类型,
// 注意,对于内置类型,这种友好关系是运允许的,便于我们用内置类型来实例化Bar。
};
}
12).类型别名。
typedef Blob<string> StrBlob;
这样就得到一个StrBlob
表示一个string
实例化的Blob
。- 为类模板定义一个类型别名。(类模板不是一个类型,但是新标准允许我们为一个类模板定义一个类型别名。形式比较特殊。)
{
template<typename T> using twin = pair<T, T>;
// 使用twin也需要指出特定的类型。
twin<int> win_loss;//win_loss是一个pair<int, int>
twin<string> area;//area是一个pair<string, string>
// 我们还可以固定一个或者多个模板参数
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;//books是一个pair<string, unsigned>
partNo<Student> kids;//kids是一个pair<Student, unsigned>
}
13).类模板的static
成员
{
template <class T> class Foo {
public:
static size_t count() {
return ctr;
}
private:
static size_t ctr;
};
}
- 每一个
Foo
的实例化都有其自己的static
成员。即,对于给定的类型X,都有一个Foo<X>::ctr
,和一个Foo<T>::count
成员实例。所有的Foo<X>
共享这个ctr
和count
函数。 - 每一个模板类的
static
成员,和其他类一样,都是有且仅有一个定义。但是由于,每一个实例类都有一个独有的对象。因此,对于static
成员于定义模板函数一致。template<typename T> size_t Foo<T>::ctr = 0;
与定义别名,类模板成员一样,以一个模板参数列表开始,然后是定义的类型和名字。 - 访问类模板的
static
成员,也必须指定哪一个实例的static
成员,或者使用对象。
{
// static成员函数也是一样的,只有使用时
// 才会被实例化。
Foo<int> f;//实例化Foo<int>类和static数据成员ctr
auto ct = Foo<int>::count();//实例化Foo<int>::count
ct = f.count();//使用Foo<int>::count
ct = Foo::count();//错误,没有指定实例
}
练习,
- 16.11,
List<T>();List<T>(const List<T> &);
这样的构造函数也是合法的,但是没有必要。
//3.模板参数
1).模板参数的名字可以是任意的。它的作用域就是声明之后到模板声明或者定义结束之前。它也会隐藏外层作用域中声明想同的名字。但是,不能使用模板参数名,这和特殊标识符号是一样的。
{
typedef double A;
template <typename A, typename B>
void f(A a, B b) {
A temp = a;//temp是A类型的
double B;//错误。
}
// 这也是重用了模板参数名
template <typename T, typename T>....
}
2).模板声明
- 必须包含模板参数。
{
template <typename> class Blob;
template <typename T> int compare(const T&, const T&);
// 与函数一致,声明中的模板参数的名字和定义不必相同,甚至可以省略
// 但是数量和类型是一致的。
// 因为声明多少次是无所谓的。
template <typename T> T calc(const T&, const T&);
template <typename T> U calc(const U&, const U&);
template <typename Type>
Type calc(const Type &a, const Type &b) {...}
}
3).当我们希望通知编译器一个名字表示类型时,必须使用关键字typename
,而不能是class
。在类型参数列表中可以用class?
- 如果没有显式地指出,c++假定通过作用域运算符号访问的名字是一个变量而不是一个类型。
- 在普通的时候,不需要这样地指出,因为编译器对类型的定义知晓,当我们使用时,它知道是哪一种情况(是类型成员还是
static
成员。)。 - 而当使用参数类型时,编译器只有当实例化时,才能知道。但是处理模板,编译器就必须知道名字是否表示一个类型。
T::size_type * p;//否则编译器不知道这是定义一个变量还是一个乘法。
- 所以,显式地指出。
{
template <typename T>
typename T::value_type top(const T &c) {
if (!c.empty())
return c.back();
else
// 注意,这里表示的是值初始化的一个变量。
return typename T::value_type();
}
}
4).默认模板实参
- 与函数默认实参一样,一个模板参数,只有它的右侧所有的参数都有默认实参时,它才可以有默认实参。
{
// 类型模板参数,有一个默认实参less<T>,使用和,调用compare,待进行比较,参数相同的类型,进行初始化的。
// 函数,有一个默认实参,less<T>()
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;
}
// 当我们使用这个模板函数时,可以提供自己的比较操作,也可不提供,使用默认的操作。
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, isbnCompare);//传递的是一个可调用对象。
// 这个可调用对象的返回值可以转换为bool
// 接受的形参可以和Sales_data兼容。
// 注意函数是通过参数的类型来推出模板的类型的。
// 这里F推出就是isbnCompare类型。
}
5).模板实参和类模版
- 不管是否需要使用默认实参,使用类模板都需要加上尖括号。尖括号指出类必须从一个模板实例化而来。
{
template <typename T = int> class Numbers {
public:
Numbers(T v = 0) : val(v) {}
private:
T val;
};
// 使用
Numbers<long double> lots_of_precious;
Numbers<> average_precision;//表示使用默认的类型。
}
//4.成员模板
1).模板类和普通类,都可以包含一个模板成员函数。这样成员称为成员模板。成员模板不可以是虚函数。
2).普通类的成员模板。
{
class DebugDelete {
public:
DebugDelete(ostream &s = cerr) : os(s) {}
template <typename T>
void operator()(T *p) const {
os << "deleting unique_ptr" << endl;
delete p;
}
private:
ostream &os;
};
// 使用。
double *p = new double;
DebugDelete d;
d(p);//调用DebugDelete::operator()(double *p)
int *ip = new int;
DebugDelete() (ip);//调用DebugDelete()::operator()(int *)
// 由于这也可以delete给定的指针,我们可以使用它作为unique_ptr的删除器。
// 实例化DebugDelete::operator()(int *)
// 重载了unique_ptr的删除器。
// 是一个可调用对象就可。
// 每一个删除unique_ptr就会实例化DebugDelete的调用运算符。
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
}
3).类模板的成员模板
- 类和成员各自有自己的,独立的模板参数。
{
// 为模板定义了一个构造函数,我们希望这个构造函数可以接受任意类型的迭代器;于是它就别定义为模板函数。
// 注意到类和函数的模板参数是相互独立的。
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
};
}
- 在类外定义一个成员模板时,必须同时提供类模板和成员模板参数列表。类模板的参数列表在前,函数的模板在数列表在后,
{
// 原因很简单,就是因为定义时需要用到。
template <typename T>
template <typename It>
Blob<T>::Blob(It b, It e) :
data(make_shared<vector<T>>)(b, e) {}
}
4).实例化一个类模板的成员模板。
- 实例化类,还是需要显式地指出;实例化函数还是由编译器隐式地从实参中推断得到。
{
int ia[] = {0, 1, 2, 3, 4};
vector<long> vi = {1, 2, 3, 4};
list<const chat *> w = {"wow", "is", "the", "time"};
// 实例化一个Blob<int>和接受两个int *的构造函数。
Blob<int> a1(begin(ia), end(ia));
// 实例化一个Blob<long>和接受两个vector<long>::iterator的构造函数。
Blob<long> a2(vi.begin(), vi.end());
// 实例化一个Blob<string>和接受两个list<const char *>::iterator的构造函数。
Blob<string> a3(w.begin(), w.end());
}
//5.控制实例化
1).由于模板是使用时才会进行实例化,如果我们在多个独立编译的源文件中都使用了相同的模板,并且提供了一样的模板参数时,每一个源文件就都会有一个该模板的实例。这样无疑是额外的开销。
2).解决办法,显式实例化。
{
extern template declaration;//实例化声明
template declaration;//实例化定义。
// 注意所有的模板参数已经替换为为模板实参。
extern template class Blob<string>;//声明
template int compare(const int &, const int &);//定义
}
- 当编译器遇到
extern
模板声明时,它不会在本文件中生成实例化代码;将一个实例化声明为extern
就表示承诺在程序其他位置有该实例化的一个非extern
声明(定义)。对于给定的一个实例化版本,可能有多个extern
声明,但是必须只有一个定义。 - 编译器使用哦一个模板时会自动对其实例化,因此
extern
声明必须出现在任何使用此实例化版本代码之前。
{
// Application.cpp
// 这些模板必须在程序的其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int &, const int &);
Blob<string> sa1, sa2;//实例化会出现在其他位置
// Blob<int>以及接受initializer_list的构造函数在本文件中实例化
Blob<int> a1 = {1, 2, 3, 4};
Blob<int> a2(a1);//拷贝构造函数在本文件中实例化,注意a2使用的是a1已经实例化的版本。
int i = compare(a1[0], a2[0]);//实例化出现在其他位置。
// 在文件Application.o文件中将会包含Blob<int>,和接受initializer_list的构造函数的实例化。
// 而compare<int>和Blob<string>类的实例化
// 不在本文件中,它们的定义必须出现在其他程序文件中。
// templateBuild.cpp
// 实例化文件必须为每一个在其他文件中的声明为extern的类型和函数,提供一个唯一的定义。
template int compare(const int &, const int &);
template class Blob<string>;
// 当编译器遇到一个实例化定义时,它为其生成代码。
// 因为templateBuild.o包含了compare的int实例化版本的定义和Blob<string>的定义。当我们编译程序时,需要将templateBuild.o和文件Application.o进行链接。
// 实例化定义的位置是无所谓的。
}
3).实例化定义会实例化所有成员
- 一个类实例化定义会实例化该模板的所有成员,包括内联的成员函数,当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。因此,如果我们显式实例化一个类模板的类型,必须能用于模板的所有成员。
练习,
- 16.27,一个文件只需要实例化以西就可以。
//6.效率和灵活性
1).unique_ptr
和shared_ptr
的设计。
- 都是模板,可以在运行时改变存储的类型。
- 对于
unique_ptr
删除器类型也是它的类型的一部分。 - 在运行时候绑定删除器,
shared_ptr
的重载比较方便;在编译器时绑定删除器,避免了间接调用的额外开销,但是重载起来比较麻烦。
练习,
- 16.28,
{
#ifndef SP_H
#define SP_H
#include <iostream>
using namespace std;
template <typename T>
class SP {
public:
SP() : p(nullptr), use(nullptr) {}
explicit SP(T *pt) :
p(pt), use(new int(1)) {}
SP(const SP &sp) :
p(sp.p), use(sp.use) {
if(use) ++*use;
}
SP& operator=(const SP&);
~SP();
T& operator*() {return *p;}
T& operator*() const {return *p;}
private:
T *p;
size_t *use;
};
template <typename T>
SP<T>::~SP() {
if(use && --*use == 0) {
delete p;
delete use;
}
}
...
template <typename T>
class UP {
public:
UP() : p(nullptr) {}
UP(const UP &) delete;//禁止进行拷贝
explicit UP(T *up) :
p(up) {}
UP& operator=(const UP&) = delete;//禁止赋值
~UP();
T* release();
void reset(T* new_p);
T& operator*() {return *p;}
T& operator*() const {return *p;}
private:
T *p;
};
}
/2.模板实参的推断
1).推断规则,
- 顶层
const
无论是在形参还是实参中,都会被忽略。(对于实参而言) - 应用于函数模板的类型转换包括以下两项(对实参进行转换)
const
转换。可以将一个非const
对象的指针或者引用传递给一个const
指针或者引用形参。- 数组或函数指针转换,如果函数形参不是引用类型,则可以对数组或者函数类型的实参应用正常的指针转换。
- 其他的类型转换,如算术转换,派生类向基类的转换,以及用户自定义的转换,都不能应用于函数模板的实参。
{
template <typename T> T fobj(T, T);//实参是被拷贝
template <typename T> T fref(const T &, const T &);//引用
string s1("a value");
const string s2("another value");
fobl(s1, s2);//调用fobj(string ,string);忽略顶层const
fref(s1, s2);//调用的是,fref(const string &, const string &)
// s1转换为const是允许的。
int a[10], b[30];
fobj(a, b);//调用的是,fobj(int *, int *)
fref(a, b);//错误,数组的类型不一致。
}
2).使用相同模板参数类型的函数形参
- 由于只能有以上有限的类型转换,当一个模板类型参数用作多个函数形参的类型时,实参必须有相同的类型。如果推断出来的类型不匹配,调用iu是错误的。
{
long lhg;
compare(lng, 1024);//错误,不能实例化compare(long, int)
//如果希望函数实参可以进行正常的类型转换,可以将函数模板定义为两个类型参数。
template <typename A, typename B>
int flexibleCompare(const A &v1, const B &v2) {
if...
}
// 使用。
long lng;
flexibleCompare(lng, 1024);//使用的是flexibleCompare(long, int)
}
练习,
- 16.34,
{
template <class T>
int compare(const T &v1, const T &v2);
// 因为参数是引用,不会有数组到指针的转换。所以导致类型不会匹配。
compare("hi", "hello");//错误,类型不匹配。
// 正确,都是const char[6]
compare("world", "error");
}
//2.函数模板的显式实参(显式实例化)
1).用户控制函数模板的实例化
- 用户自定义结果的类型。
{
// 编译器无法推断出T3,因为它未出现在函数参数列表中。
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
// 由于没有任何函数实参的类型可用来推断T1的类型,每一次调用sum时调用之都必须为T1提供一个 显式模板实参
// 提供的方式和和定义类模板实例的方式相同,显式模板实参在尖括号中给出,位于函数名之后,实参列表之前
auto val3 = sum<long long>(i, lng);//long long sum(int, long)
// 这里显式指定了T1的类型,而T2和T3的类型有编译器i从i和lng的类型推断出来。
// 注意显式模板实参按照由左至右的顺序与对应的模板参数匹配
// 第一个参数模板实参与第一个模板参数匹配,第二个模板实参和第二个模板参数匹配...
//只有尾部参数的模板实参才可以忽略。
// 而且前提是它可以从函数参数中推断出来
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2,T1);
// 则,此时我们必须为三个模板参数指定实参
auto val3 = alternative_sum<long long>(i, lng);//错误,不能推断第三个模板参数
auto val2 = alternative_sum<long long, int, long>(i, lng);
}
2).正常类型转换可以应用于显式指定的实参。
{
long lng;
compare(lng, 1024);//错误,模板参数不匹配
compare<long>(lng, 1024);//实例化compare(long, long)
compare<int>(lng, 1024);//实例化compare(int, int) 可以进行类型转换。
// 显式指定的参数,将会 直接替换 模板参数列表里的参数
}
练习,
- 16.38,为什么
make_shared
需要显式地实参。有时候我们需要的是默认初始化,有时候会用make_shared<string>(10,'4');
的形式进行初始化。而以上的形式编译器都没有办法得到实参的类型。 - 16.39,比较i啷个字符串的大小,可以这样做,
compare<string>("hello", "hi");
如果没有显式地指定,编译器自动识别的将会是,char []
,不仅仅如此,而且得到的很可能是类型不匹配。
//3.位置返回类型和类型转换
1).显示地指出返回类型虽然有一定的好处。但是也有问题。例如,当我们需要返回迭代器范围中的指定元素时,需要返回的类型就是元素的类型,可是我们不知道元素的类型。我们可以使用decltype(*b)
但是,在函数参数列表之前,b是不存在的。
- 为了定义上面的函数,我们必须使用位置返回类型。由于位置返回类型在参数列表之后,它可以使用函数的参数。
{
// 注意,解引用返回的是一个左值
// 所以推断出元素的类型是引用。
template <typename It>
auto fcn(It b, It e) -> decltype(*b) {
/*....*/
return *b;
}
}
2).进行类型转换的标准库模板类
- 问题,我们有时候需要返回一个元素的值而不是它的引用。但是我们传入的是迭代器,对元素的类型一无所知,并且迭代器解引用只能返回元素的引用。
- 解决,使用标准库的类型转换模板。
- 它们定义在头文件
type_traits
。这个头文件中的类通常用于所谓的模板元程序设计,但是对于普通的编程也很有用。 - 使用
remove_reference
来得到元素类型。它有一个模板类型参数,和一个名为type
的public
类型成员。使用,remove_reference<int &>
则type
成员将会是int
。即如果我们用一个引用类型实例化它,它的type
成员将会表示被引用的类型。 - 如果给定的是一个迭代器。
remove_reference<decltype(*b)>::type
将会得到元素的类型。
{
template <typename T>
auto fcn2(It b, It e) ->
typename remove_reference(*b)::type {
//处理
return *b;//返回一个值,元素的一个拷贝,而不是一个引用
}
}
3).标准库类型转换模板。p606
练习
- 16.40,注意
decltype(*b + 0);
- 元素类型必须支持加法的
- 由于
*b + 0
返回的是右值,返回的将会是,const
右值引用
//4.函数指针和实参推断
1).用函数模板初始化一个函数指针或为一个函数指针赋值。
{
template <typename T>
int compare(const T&, const T&);
// pf1指向的是实例化的
// int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
// pf1中的参数类型决定了T的模板实参类型。
// 编译器通过推断指针的类型来得到模板实参。
// 如果不能从函数指针类型确定模板实参,则会产生错误
// 两个重载的版本,接受不一样的函数指针
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare);//错误,使用的是哪一个compare的实例
// func的参数类型无法确定模板实参的唯一类型。编译失败。
// 解决办法,显式地实例化
func(compare<int>);//调用的是compare(int (*)(const int&, const int&));
}
- 当参数是一个函数模板的实例地址时,程序上下位必须满足,对每一个模板参数,能唯一确定其类型或者值。
//5.模板实参推断和引用
1).左值引用函数参数推断类型
{
//函数形参是 T&形式的
template <typename T>
void f1(T &);//实参必须时一个左值,一个变量或者一个返回引用类型的表达式
f1(i);//i是一个int,T为int
f1(ci);//ci是一个const int,T为const int。这又有绑定规则决定的。
f1(2);//错误,传递给引用的参数必须是要给左值。
// 如果函数参数是const T&
// 绑定规则,实参可以是一个const或者非const的对象
// 可以是一个临时对象或者一个字面值常量
// T永远不会是const
template <typename T>
void f2(const T&);//可以接受一个右值
f2(i);//i是一个int,T为int
f2(ci);//ci是一个const int,T为int
f2(5);//5是一个字面值,T也是一个int
}
2).右值引用函数参数推断类型
template <typename T> void f3(T &&);
f3(12);
推断出来T为int
3).两个左值赋值给右值引用变量的例外- 这两个例外时
move
标准库正常工作的基础
- 第一个例外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数时,且此右值引用指向模板类型参数(T&&)时,编译器推断模板类型参数为实参的左值引用类型。因此
f3(i);
编译器推断出T的类型,int&
而不是int
。这样推断,意味着f3的函数参数应该是一个类型为int&
的右值引用。通常我们不能(直接)定义一个引用的引用。但是通过类型别名或通过模板类型参数间接定义是可以的 - 第二个例外的绑定规则,入股哦我们间接创建一个引用的引用,则这些引用形成了折叠。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有一种特殊的情况下引用折叠会成右值引用:右值引用的右值引用。
{
// 对于一个类型X
X& &, X&& &, X& &&;//都折叠成X&;
X&& &&;//折叠成X&&
// 引用折叠只能用于间接创建的引用的引用,例如类型别名或者模板参数
}
3).利用引用折叠规则和右值引用的特殊类型推断规则结合在一起。我们可以对一个左值调用f3.
{
f3(i);//i是一个int,T为一个int &
f3(ci);//ci是一个const int
// T是一个const int&
// f3(i)的实例化可能就是
void f3(int& &&);//因为T被推断出为int&
// 由折叠规则,int& &&折叠成int &
// 因此,虽然函数的参数是一个右值引用形式,但是此调用会用一个左值引用类型。即,用int&实例化f3
void f3<int &>(int &);
// 这两个规则,导致了一个重要结果
// 1. 如果一个函数参数是一个指向模板类型参数的右值引用(T &&),则它可以接受一个左值,进行绑定
// 2. 并且,如果实参是一个左值,推断出来的模板实参类型是一个左值引用,并且函数参数被实例化为一个普通的左值引用参数(T &)
// 这意味着,对于指向模板类型参数的右值引用函数参数,我们既可以传递一个右值,也可以传递一个左值。
}
4).编写接受右值引用参数的模板函数
{
template <typename T>
void f3(T &&val) {
T t = val;//拷贝还是绑定一个引用?
t = fcn(t);//赋值值海边t还是既改变t又改变val
// 如果T是引用,则一致为true
if (val == t) {
}
}
// 这使得编写正确的代码变得异常困难
// 虽然类型转换模板类可能会又一些帮助。
// 在实际中,右值引用通常用于两种情况,模板转发其实参和模板被重载。
// 目前应该注意的是,使用右值引用的函数模板通常使用以下的方式进行重载
template <typename T> void f(T &&);//绑定到非const的右值
template <typename T> void f(const T &);//绑定左值和const右值。
// 以上的重载方式和非模板函数是一样的。
}
练习,
- 16.45,引用不是对象,它没有地址,指针不能指向它,容器
vector
也不能容纳它。因为vector
本质上就是用指针进行的动态内存的管理。
//6.理解std::move
1).它是使用右值引用模板的一个很好的例子。它可以接受任何实参,它是一个函数模板。
2).如何定义std::move
{
// 标准库的定义
template <typename T>
typename remove_reference<T>::type&& move(T &&t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
// 注意在类型之前加上typename
// move的函数参数是要给T&&(指向函数模板参数的右值引用),通过引用折叠,可以接受任何实参。
//使用
string s1("hi!"), s2;
s2 = std::move(string("bye!"));//正确从一个右值中移动数据
// T为string
s2 = std::move(s1);//合理,但是同时进行了类型转换。
// return语句,对s1的引用t(左值引用类型)(注意引用折叠)进行了强转为右值引用。所以可能会修改值。
// 工作过程略
}
3).以上说明从一个左值到右值的转换是允许的。
- 虽然我们不可以隐式地将左值转换为一个右值引用;但是我们可以使用
static_cast
显式地将一个左值转换为一个右值引用。 - 这样显式地指出,可以方式我们意外地进行这样的左值到右值的截断转换;迫使我们嫩保证这样的转换是安全的。
- 尽管这样的方式是允许的,我们还是统一使用
std::move
,方便排错。
//7.转发
1).某一些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括是否是const
是左值还是右值。
{
// 接受一个可调用表达式和两个额外实参。
// 函数调用给定的可调用对象
// 两个实参逆序传递给他
// 但是这样做,将会导致,顶层的const丢失
template <typename F, typename T1, typename T2>
void flipl(F f, T1 t1, T2 t2) {
f(t2, t1);
}
// 注意这里v2是一个引用,
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
f(42, i);//改变了实参
flipl(f, 42, i);//通过flipl调用f不会该部件i
}
2).以上的调用不能达到我们想要的目的。
- 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。(左值性,
const
性质。)
- 使用引用参数,使得我们可以保持底层的
const
性质 - 使用右值引用可以通过引用折叠保留实参的左值,右值性质。
{
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
// 问题,如果,我们的可调用对象的形参是右值引用,他不可以接受左值。如何解决。
}
3).在调用中使用std::forward保持类型信息。
- 它是一个标准库设施。它和
move
一样定义在头文件utility
中;与move
不一样,它必须通过显式模板实参来调用。forward
返回该显示实参的乐星的左值引用。即,forward<T>
的返回值是一个T&&
- 通常情况下,我们使用
forward
传递那些定义为模板类型参数的右值引用的函数参数。通过返回类型上的引用折叠,forward
可以保持给定实参的右值或者左值属性。
{
template <typename T>
intermediary(Type &&arg) {
finalFcn(std::forward<Type>(arg));
}
// Type表示传递给arg实参的所有类型信息
// 如果传入的实参是一个左值,
// Type是const int&
// arg 是一个const int&
// 通过引用折叠,forward返回的是一个const int&
// 如果传入的是一个右值,
// Type是一个const int
// 返回的是一个const int &&
// 重写函数
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
// 如果调用,flip(f, 1, 42);
// i将会以int &的形式传递给f
// 42将会以int &&的形式传递给f
// 以forward的返回值的类型传递给函数。
}
/3.重载与函数模板
1).函数模板可以被另一个模板或一个普通非模板函数重载。名字相同的函数必须又不同数量或者类型的参数。
2).如果涉及到函数模板,函数匹配的规则会在以下方面受到影响。
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的模板总是可行的,因为模板实参推断会会排除不可行的模板。
- 可行函数(模板或者非模板),按照类型转换来排序。但是可用于函数模板的类型转换非常有限。
- 如果且有一个函数提供比其他任何函数都更好的匹配,则选择该函数。但是
- 如果同样好的函数中,只有一个非模板函数,则选择此函数。
- 如果同样好的函数中,没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更加特例化,则选择该模板。
- 否则该调用有歧义。
3).编写重载函数模板
- 在调试中可能很有用的一组函数。
{
// 打印我们不能处理的类型
template <typename T>
string debug_rep(const T &t) {
ostringstream ret;
ret << t; //使用T的输出运算符,打印一个t的表示形式。
return ret.str();//返回ret绑定的一个副本。
}
// 此函数可以用来生成一个对象对应的string表示,
// 该对象可以是任意具备输出运算符号的类型。
// 打印指针的值,后跟指针指向的对象
// 注意此函数不能用于char*,否则返回的就是一个字符。
// 并且更加重要的是,不可以打印字符指针自身的值,因为IO库定义为char*定义了<<版本,此输出运算符的作用是假定指针表示一个以空字符结尾的字符数组,并打印该字符数组,而不是地址值。
template <typename T>
string debug_rep(T *p) {
ostringstream ret;
ret << "pointer: " << p;//打印指针的地址
if (p)
ret << " " << debug_ret(*p);//打印p指向的值
else
ret << "null pointer."
return ret.str();//放回ret绑定的string的一个副本。
}
// 我们可以这样处理。
// 这个调用只能调用第一个函数模板,
// 因为不管怎么样,我们都不可能将一致非指针实力胡啊一个期望指针的函数模板。
string s("hi");
cout << debug_rep(s) << endl;
// 如果我们这样调用
cout << debug_ret(&s) << endl;
// 两个版本都是可以的。
// 第一个版本得到的是
debug_ret(const string* &);//T是一个string*
// 第二个版本得到的。
debug_ret(string *);//T是一个string
// 由于第二个版本是一个精确匹配,而
// 第二个版本需要有一个从非const到const的转换
// 所以最终选择第二个版本
}
4).多个可行的版本
{
const string *sp = &s;
cout << debug_ret(sp) << endl;
// 第一个的版本的实例化
debug_ret(const string * &);//T是一个string *
// 第二个版本,T是一个const string
// 此时两个都是精确的匹配,正常的匹配规则无法区分两个函数。
// 但是根据重载函数模板的规则,此调用被解析为,更加特例化的debug_ret(T*)
// 为什么设计这条规则,如果没有他,那么我们将会无法对一个const指针调用指针版本的debug_ret。问题在于,模板debug_ret(const T&),本质上可以用于任何类型。包括指针类型
// 此模板比debug_ret(T*)更加通用,后者只能用于指针类型。
// 如果没有这个规则,那么后者的调用永远都是有歧义的
}
5).非模板和模板的重载
{
string debug_ret(const string &s) {
return '"' + s + '"';
}
// 调用
string s("hello");
cout << debug_ret(s) << endl;
// 有两个一样好的可行函数。
// 第一个版本。
debug_ret<string>(const string &);//T绑定到string
debug_ret(const string &);//非模板函数
// 我们会选择第二个版本,当存在多个同样好的函数时,
// 编译器会选择最特例化的版本。
// 所以编译器会选择一个非模板版本而不是一个函数模板
}
6).重载模板和类型转换
- C风格字符串指针和字符串字面量我们尚未讨论。
{
cout << debug_ret("hello") << endl;//调用debug_ret(T*)版本
debug_ret(const T&);//T被实例化为一个,char[6]
debug_ret(T *);//T被实例化为const char
debug_ret(const string &);//要求从const char*到string的类型转换。
// 两个模板都是精确的匹配,第二个需要数组到指针的转换,对于函数的匹配这样的转换是允许的,也是精确匹配。
// 出于特例化的要求,第二个模板比第一个模板更加特例化
// 第三个虽然可行,但是依赖用户自己定义的类型转换。它没有达到精确匹配
// 如果我们希望字符串指针按照string来处理
// 定义这两个重载的版本。
// 字符串数组和字符串字面量都是一个const量,不需要两个函数。
string debug_ret(char *p) {
// 将字符串指针转换为string
// 并使用string的版本进行处理
return debug_ret(string(p));
}
string debug_ret(const char*) {
return debug_ret(string(p));
}
}
7).缺少声明可能导致程序异常
- 在定义任何一个函数之前,记得声明你所需要的所有重载的版本。这样就不必担心,编译器由于未遇到你希望的版本,导致实例化一个你并不需要的版本。
练习
- 16.49,T可以绑定为一个指针。
/4.可变参数模板
1).一个可变参数模板就是一个接受可变数目参数的模板函数或者类。可变参数称为参数包。有两个中参数包
- 模板参数包,表示零个或者多个模板参数
- 函数参数包,表示零个或者多个函数参数
2).我们使用省略号来指出一个模板参数或者函数参数的包。在一个模板参数列中,class…或者typename…指出接下来的参数表示零个或者多个类型的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
{
// Args是一个模板参数包,rest是一个函数参数包
// Args表示零个或则多个模板参数列表
// rest表示零个零个或者多个函数参数。
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
// 声明了foo是一个可变参数函数模板,它有一个名为T的的类型参数,和一个名为Args的模板参数包,这个包表示零个或者多个额外的类型参数。
// foo函数参数列表包含一个const&的参数,指向T类型,还包含一个名为rest的函数参数包。
// 使用,
int i = 0;
double d = 2.22;
string s = "how new";
foo(i, s, 24, d);//包含三个参数
foo(s, 42, "hi");//包含两个参数
foo(s, d);//包含一个参数
foo("hi");//空包
// 编译器实例化的四个版本
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const string&, const double&);
void foo(const char[3]&);
// 每一个实例,T的类型都是从第一个实参推断出来的,剩下的实参提供给函数额外的类型参数。
// 注意到保留了const属性和&属性
}
2).sizeof...
运算符
- 当我们需要知道包里面有多少个元素时,可以使用
sizeof...
运算符。类似于,sizeof
,返回一个常量值,而且不会对实参进行求值。
{
template <typename...Args>
void g(Args...args) {
cout << sizeof...(Args) << endl;//类型参数的数目
cout << sizeof...(args) << endl;//函数参数的数目
}
}
//1.编写可变参数函数模板
1).解决实参的数目和类型均未知的情况。
- 使用
initializer_list
的所有实参必须有一样的类型或者可以转换为一样的类型。
2).一个例子。 - 可变参数函数通常是递归的,第一步调用,处理包中的第一个实参,然后用剩余实参调用自身。设计的
print
也是这样的模式每一个递归调用将第二个实参打印到第一个实参表示的流中。为了终止递归,我们还需要定义一个非可变参数的print
,他接受一个流和一个对象。
{
// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数版本的print定义之前声明
template <typename T>
ostream& print(ostream &os, const T &t) {
return os << t; //包中最后一个元素之后不打印分割符
}
// 除了包中的最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args &rest) {
os << t << ", "//打印第一个实参
return print(os, rest...);//递归调用,打印其他的实参
// 可变参数版本的print函数接受三个参数
// 但是我们只是传递两个实参,结果是,rest中的第一个实参被绑定到t
// 剩余的实参形成下一个print的调用的参数包
// 因此,在每一个调用中,包中的第一个实参被移除,成功绑定到t的实参。
// 例如,
print(cout, i, s, 42);//包中含有两个参数
// 最后一步时,只传递两个参数,由于两个参数的函数更加特例化
// 所以编译器选择该函数
// 当定义可变参数print时,非可变参数版本的声明必须在作用域中,
// 否则将会导致可变参数版本无线递归
}
}
//2.包扩展
1).对于一个参数包,我们可以获取其大小,还能对他做的唯一一件事情就是**扩展。**当我们扩展一个包时,我们还有提供用于每一个扩展元素的模式。扩展一个包就是将他分解为构成的元素,对每一个元素应用模式,获取扩展后的元素。
- 通过在模式右边放一个省略号来触发扩展操作。
{
template <typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest) { //扩展了Args
os...
return print(os, rest...);//扩展rest
}
// 第一个扩展扩展模板参数包,为print生成函数参数列表
// 第二扩展为实参,为print调用生成实参列表。
// 在Args的扩展中,编译器将const Args&的模式应用到模板参数包Args中的每一个元素。因此,此模式的扩展结果就是一个逗号分割的零个或者多个类型的列表
// 每一个类型都形如,const type&。
print(cout, i, s, 42);//一个是string一个是int
// 最后实例化的是
ostream&
print(stream &, const int &, const string &, const int &);
// 第二个扩展,模式就是函数参数包的名字,此扩展就扩展出一个包中元素组成的,由逗号分割的列表。
print(cout, s, 42);//等价于该形式
}
2).理解包扩展
- 以上的
print
例子就仅仅是将包中的元素扩展为其构成的元素,还可以有更加复杂的扩展。
{
template <typename... Args>
ostream& errorMsg(ostream &os, const Args&... rest) {
return print(os, debug_ret(rest)...);
}
// 模式是debug_ret,该模式表示我们希望对函数参数包rest中的每一个元素都调用debug_ret。扩展结果就是一个逗号分割的debug_ret调用列表。
errorMsg(cerr, fcnName, code,num(),。。。。。)
// 就是调用debug_ret的返回值。
// 注意下面的模式是错误的
print(cerr, debug_ret(rest...));
// 这段的代码的问题是,我们在debug_ret的参数列表中扩展了rest
// 而debug_ret并没有接受这样多参数的版本,将会导致调用失败。
}
//3.转发参数包
1).在新标准下,我们可以使用可变参数模板于forward
机制来编写函数。
2).以StrVec
中的成员函数emplace_back
作为例子,他是一个可变参数函数,在容器管理内存空间中构造一个元素。
- 为了保持类型信息。
emplace
中参数都是参数模板的右值引用。
{
class StrVec {
public:
template <typename... Args>
void emplace_back(Args&&...);
};
}
- 当
emplace_back
传递参数给construct
时,我们还需要保证,还原参数的类型。
{
// 使用forward
template <typename...Args>
inline
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc();//查看是否需要扩展空间
alloc.construct(first_free++, std::forward<Args>(args)...)
}
// 值得注意的是
// std::forward<Args>(args)...
// 包含两种的扩展,
// 他既扩展了模板参数包Args,也扩展了函数参数包args。
// 生成的形式就是
std::forward<Ti>(ti);
// 使用
sevc.emplace_back(10, 'c');
// construct的调用模式会扩展出该形式
std::forward<int>(10), std::forward<char>('c')
svec.emplace_back(s1 + s2);
// 传递给construct的扩展将会是
std::forward<string>(string("the end"));
// 该类型将会是,string&&,将会得到一个右值引用实参。
// 此时,construct会继续将此实参传递给string的移动构造函数,构建新元素。
}
3).所有的可变参数转发,都有和emplace_back
类似的形式。
- 参数是右值引用,可以接受任意的类型参数。
- 配合
forward
可以将参数的类型信息完整的转发。
练习,
- 16.59,是
forward<string&>
结果是string&
答案解析错误。 - 16.61,编写
make_shared
{
template <typename T, typename... Args>
SP<T> make_SP(Args&&... args) {
return SP<T>(new T(std::forward<Args>(args)...));
}
}
/5.模板特例化
1).当我们不能(或者不希望)使用模板版本时,可以定义类或者函数模板的一个版本特例化。
- 编写更加高效的代码
- 解决通用模板无法正确解决或者正确编译的问题。
2).例如,我们希望比较两个字符指针的内容而不是指针值,通用的版本不能做到这一点。
{
template <typename T>
int compare (const T&, const T&);//第一个版本可以比较任意类型相同的
template <size_t N, size_t M>
int compare(const char (&v1)[N], const char (&v2)[M]);
// 第二个版本可以指针字符串字面量
// 如果我们传入的参数是字符串字面量或者是字符数组,编译器会调用第二个版本
// 如果传递的是一个字符指针,则会调用第一个版本,因为无法是实现转换
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);//第一个版本
compare("hi", "mom");//第二个版本
// 我们无法将一个指针转换为数组的引用。第二个版本不可行
// 为了处理字符串指针,可以为第一个版本定义一个 模板特例化版本
// 一个模板特例化就是模板的一个独立的定义,在其中一个或者多个模板参数被指定为特定的类型
// 当我们特例化一个函数模板时,必须将原模版中的每一个模板参数都提供实参
// 为了指出我们在实例化一个模板,
// 应该使用关键字加上空的尖括号
// 空的尖括号指出我们为原模版的所有模板实参提供实参
// 模板的extern声明或者定义是不需要尖括号的。
template <>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
// 特例化时,函数参数类型必须与一个先前声明的模板中对应类型匹配。
// 我们特例化的是
template <typename T>
int compare(const T&, const T&);
// 其中函数参数为一个指向const类型的引用。
// 我们特例化的是一个指向const char 的const 的指针的引用。
// 为什么要是const指针,因为要和函数模板一致。
// 指针的const版本是一个常量指针而不是一个指向常量的指针。
}
3).函数重载和模板实例化
- 函数重载也可以实现模板实例化一样的功能,为什么要进行特例化???
- 当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模版的一个特殊实例提供了定义。一个特例化版本本质上是一个实例,而不是一个函数名的一个重载版本。所以特例化不会影响函数匹配。
- 我们将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配。
- 例如,我们定义了两个版本的
compare
函数模板,
{
// 接受两个数组的引用
template <size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
// 接受const T*
template <typename T>
int compare(const T*, const T*);
// 我们还定义了一个特例化版本来处理字符指针。这对函数匹配没有关系。
// 例如,当发生以下调用时
compare("hi", "mom");
// 两个函数模板都是可行的,都不需要转换,提供一样的精确匹配。
// 但是由于接受字符数组参数的版本更加特例化,因此我们会选择它
// 此时,如果我们将接受字符指针的compare版本定义为一个普通的非函数模板(而不是一个函数模板的特例化。)
//此时将会有三个可行函数。两个模板,还有一个非模板函数
// 它们提供一样好的精确匹配
// 此时会选择普通的函数。
}
4).函数模板的特例化不会影响函数匹配过程,但是**它相当于帮助编译器进行了模板的显式实例化过程,当我们使用到该模板的该实例情况时,会调用我们已经特例化的版本,而编译器不会再实例化一个版本。**例如,我们已经定义了一个特例化的接受const char* const &
的版本,当我们传入如上面的字符串时,假如函数匹配到这个接受const T&
的函数模板时,会使用我们特例化的版本,而不是再进行实例化。
5).如果我们没有再使用之前进行特例化的声明,那么不同于普通的类或者函数,有可能不会报错,编译器可以使用原模版进行实例化。这种错误要避免。具体情况待验证。
6).所有的模板和模板特例化声明应该放在同一个头文件中,所有同名的模板声明放在前面,然后是该模板特例化的版本。保证特例化时,函数的模板是可见的。
7).类模板的特例化。
- 特例化标准库
hash
,默认情况下它是使用hash<key_type>
来组织元素。 - 为了让我们自己的数据类型也能使用这样的默认组织形式,必须定义
hash
模板的一个特例化版本。一个特例化版本的hash
必须定义
- 一个重载调用运算符,它接受一个容器关键字类型对象,返回一个
size_t
; - 两个类型成员,
result_type
和argument_type
分别表示调用运算符的返回类型和参数类型 - 默认构造函数和拷贝赋值运算符(可以是隐式定义的)
{
// 在定义特例化版本的hash时,必须在原模版定义所在的命名空间中特例化
// 我们可以向命名空间中添加成员,
// 首先打开命名空间
namespace std {
}//关闭命名空间,值得注意的时,这里没有;
// 花括号对之间的任何定义都将称为命名空间std中的一部分
// 以下定义一个能处理Sales_data的特例化版本的一个hash
namespace std {
template <>//定义一个特例化版本,模板参数为Sales_data
struct hash<Sales_data> {
// 定义必须的条件
typedef size_t result_type;
typedef Sales_data argument_type; //默认情况下,需要定义==
size_t operator()(const Sales_data &s) const;
//使用合成的默认拷贝赋值和构造函数。
};
// 和任何其他类一样,我们可以在类内或者类外定义成员。
// 重载的掉哟共运算符必须为给定类型的值定义一个哈希函数。对于一个给定的值,任何时候调用的结果都是一样的。
size_t
operator()(const Sales_data &s) const {
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
// 这里我们将定义一个好的哈希函数的复杂任务交给标准库
// 标准库为内置类型和很多标准库类型定义了hash类的特例化把那本。
// 我们利用它们生成的哈希值进行异或运算,形成给定Sales_data对象的完成哈希值。
// 注意,默认情况下,为了处理特定的关键字类型,无需容器会组合使用key_type对应的特例化的hash版本和key_type上的运算符。
// 这使得我们定义在Sales_data上的operator==和这个hash<Sales_data>是兼容的。
}
}
8).hash
特例化版本的使用。
{
// 假定我们使用的特例化版本在作用域中,当Sales_data作为容器的关键字时,编译器会自动使用此特例版本
// 并且是使用Sales_data的==运算符号
unorder_multiset<Sales_data> SDset;
// 由于hash<Sales_data>使用了私有成员,需要声明为友元
template<class T> std::hash;//友元声明所需要的,必须保证模板版本在特例化版本中是可见的
class Sales_data {
friend class std::hash<Sales_data>;
};
// 由于,该实例定义在std命名空间中,我们必须加上std。
}
9).类模板的部分特例化
- 与函数模板不同的是,我们可以为类模板只提供一部分的而不是所有的模板参数,或者是参数的一部分而不是全部的特性。
- 一个类模板的部分特例化本身也是一个模板,使用它的用户还必须为那么特例化中未指定的模板参数提供实参。
{
// 原始的,通用的版本
template <class T> struct remove_reference {
typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
// 左值引用
template <class T> struct remove_reference <T&> {
typedef T type;
};
// 右值引用
template <class T> struct remove_reference <T&&> {
typedef T type;
};
// 由于一个部分特例化版本还是一个模板,我们首先定义一个模板参数
// 部分特例化模板的名字和原模版一致
// 对每一个未完全确定类型的模板参数,在特例化版本中的模板参数列表都有一项与之对应。
// 在类名之后就是我们指定的实参。
// 部分特例化版本的模板参数(形参列表<>里的)是原始模板的一个子集或者特例化。
// 本例中,部分特例化版本的模板参数与原始模板一样,但是类型不一样,一个是左值引用,一个是右值引用。
int i;
// decltype(23)的返回值是int
// 使用的是原始的模板
remove_reference<decltype(42)>::type a;
// 返回的是一个int&,使用的是第一特例化的版本
remove_reference<decltype(i)>::type b;
// 使用的是第二个特例化的版本
remove_reference<decltype(std::move(i))>::type c;
// 三个变量均为int类型。
}
10).特例化成员而不是类
{
template <typename T> struct Foo {
// T()是否合法?
Foo(const T &t = T()) : men(t) {}
void Bar() {}
T mem;
};
template <> //特例化一个模板
void Bar<int>::bar() {
/*.....*/
} //特例化Foo<int>的成员Bar
// 我们只特例化Foo<int>类的一个成员,其他成员将由Foo提供
Foo<string> fs;//实例化Foo<string>::Foo()
fs.Bar();//实例化Foo<string>::Bar()
Foo<int> fi;//实例化Foo<int>::Foo()
fi.Bar();//使用我们特例化的版本。
}
练习
- 16.63,
string
传递时候,会转换为char*
??