C++ Primer学习笔记-----第十六章:模板与泛型编程
模板是C++中泛型编程的基础。
模板是蓝图,用来创建类型,创建的类型就是模板的实例,就好像我们用一个类型创建相应的实例一样。
函数模板
template<typename T> //模板参数列表
void fun(T &v){}使用:
fun<int>(3);
fun(3); //自动推断类型*****非类型模板参数*****
一个非类型参数表示一个值而非一个类型,通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在
编译时实例化模板。例如:compare处理字符串字面常量
template<unsigned N,unsigned M> //N和M是非类型参数
int compare(const char (&p1)[N],const char (&p2)[M]) //由于不能拷贝数组,参数是数组的引用
{return strcmp(p1,p2);
}compare("hi","world");
编译器会使用字面常量的大小来代替N和M,从而实例化模板。
记住:编译器会在一个字符串字面常量的末尾插入一个空字符作为终结符,因此编译器会实例化如下版本:
int compare(const char (&p1)[3],const char (&p2)[4]);template<typename K, int T> //T是非类型参数
void fun(K &k)
{cout << T;
}fun<int,3>(666); //3是绑定到非类型参数的实参一个非类型参数可能是:
1.一个整型
2.一个指向对象或函数类型的指针或引用绑定到非类型参数的实参必须是一个常量表达式。
绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用一个普通(非static)局部变量或动态对象作为指针或引用
非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。*****编写类型无关的代码*****
模板程序应该尽量减少对实参类型的要求例如:compare函数
template<typename T>
int compare(const T &v1,const T &v2)
{if(v1<v2) return -1;if(v2<v1) return 1;return 0;
}上面的compare函数虽然简单,但它说明编写泛型代码的两个重要原则:
1.模板中的函数参数是const的引用。
2.函数体中的条件判断仅使用<比较运算。通过将函数参数设定为const的引用,保证了函数可以用于不能拷贝的类型。
大多数类型,包括内置类型和我们已经用过的标准库类型(除unique_ptr和IO类型之外),都是允许拷贝的。
但不允许拷贝的类类型也是存在的。通过将参数设定为const的引用,保证了这些类型可以用我们的compare函数来处理。
如果compare用于处理大对象,这种设计策略还能使函数运行的更快。
只使用<运算符就降低了compare函数对要处理的类型的要求。这些类型只需支持<即可。
实际上,如果我们真的关系类型无关和可移植性,可能需要用less来定义我们的函数:
template<typename T> int compare(const T &v1,const T &v2)
{if(less<T>()(v1,v2)) return -1; //less<T>()创建一个对象:函数对象if(less<T>()(v2,v1)) return 1;return 0;
}
less<T>的默认实现用的是<,所以这其实并未起到让这种比较有一个良好定义的作用。
模板编译
当编译器遇到一个模板定义时,并不生成代码,而是当我们实例化出模板的一个特定版本时,编译器才会生成代码。
只有当我们使用模板时,编译器才生成代码,这一特性影响了如何组织以及错误何时被检测到。通常,当我们调用调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的
但成员函数的定义不必已经出现。因此,我们将类的定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此与非模板代码不同,模板的头文件
通常即包括声明也包括定义。注:函数模板和类模板成员函数的定义通常放在头文件中
类模板
与函数模板不同,编译器不能为类模板推断模板参数类型。定义:
template<typename T> class Test
{public:Test();void fun(T &p){}; //声明并定义void fun1(); //声明
private:T meb1;shared_ptr<vector<T>> data;};构造函数定义
template<typename T>
Test<T>::Test():data(make_shared<vector<T>>()){}fun1成员函数定义,数据成员默认初始化
template<typename T>
void Test<T>::fun1()
{/* 函数体 */}实例化:
Test<int> ti;类模板成员函数只有当程序用到它时才进行实例化,如果一个成员函数没有被使用,则它不会被实例化。*****在类代码内简化模板类名的使用*****
当处于一个类模板的作用域中时,编译器处理模板自身引用时就好像已经提供了与模板参数匹配的实参一样
template<typename T> class A
{public:A& operator++(); //返回A&,而不是A<T>&,可以简化代码编写A& operator--();
};*****在类模板外使用模板名*****
template<typename T>
A<T> A<T>::operator++(int) //后置递增,返回类型不再作用域内,所以要提供模板实参T
{A ret = *this; //等价A<T> ret = *this;++*this;return ret;
}
注:在一个模板的作用域内,可以直接使用模板名儿不必指定模板实参
类模板和友元
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。
如果友元自身是模板,类可以授权所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
template<typename> class A; //B要用A,所以要先声明A
template<typename> class B; //非成员函数operator==要用B,所以要先声明B
template<typename T> bool operator==(const B<T>&,const B<T>&);
template<typename T> class B
{//B的模板形参是什么类型,A也是什么类型friend class A<T>; //友元的声明用B的模板形参作为它们自己的模板实参,关系被限定在用相同类型实例化的operator friend bool operator==<T>(const B<T>&,const B<T>&);
};
通用和特定的模板友好关系
template<typename T> class Pal; //前置声明,将模板的一个特定实例声明为友元时要用到
class C //普通类
{friend class Pas<C>; //用C实例化的Pal是C的一个友元template<typename T>friend class Pal2; //pal2的所有实例都是C的友元;这种情况无须前置声明
};template<typename T> class C2
{friend class Pal<T>;template<typename X> friend class Pal2; //模板参数不同,pal2的所有实例都是C2的每个实例的友元,不需要前置声明friend class Pal3; //不需要pal3的前置声明
};
令模板自己的类型参数成为友元
template<typename T> class A
{friend T; //将访问权限授予用来实例化A的类型
};
T是A<T>的友元
模板类型别名
typedef A<int> AT;
由于模板不是一个类型,不能定义一个typedef引用一个模板
新标准运行我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T,T>;
twin<string> myPairs; //myParis是一个pair<string,string>也可以固定一个或多个模板参数
template<typename T> using partNo = pair<T,unsigned>;
模板参数
模板参数与作用域
typedef double A;
template<typename A> void f(A a)
{A temp = a; //temp类型W诶模板参数A的类型,而非double
}*****模板声明*****
模板声明必须包含模板参数:
template<typename T> int compare(const T&,const T&);
template<typename T> class Blob;
声明中的模板参数的名字不必与定义中相同:
template<typename T> void fun(const T&); //声明
template<typename K> void fun(cosnt K&){/************/}; //定义
使用类的类型成员
用作用域运算符::来访问static成员和类型成员。
在普通(非模板)代码中,编译器掌握类的定义。因此,它知道通过作用域运算符访问的名字是类型还是static成员。
例如,如果我们写下string::size_type,编译器有string的定义,从而知道size_type是一个类型。但对于模板代码就存在困难。例如,假定T是一个模板类型参数,当编译器遇到类似T::mem这样的代码时,它不会知道mem是一个类型成员
还是一个static数据成员,直至实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。
例如:假定T是一个类型参数的名字,当编译器遇到如下形式的语句时:
T::size_type * p;
它需要知道我们是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘。默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显示
告诉编译器该名字是一个类型。使用关键字typename来实现这点:
template<typename T>
typename T::value_type top(const T& c){}
返回的类型是T::value_type
默认模板实参
template<typename T,typename F = less<T>> //F表示可调用对象,提供了默认实参
int compare(const T &v1,const T &v2,F f = F()) //f绑定到一个可调用对象上,也为函数参数提供了默认实参
{if(f(v1,v2)) return -1;if(f(v2,v1)) return 1;return 0;
}
只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
模板默认实参与类模板
template<class T = int> class A {}A<long> a;
A<> a2; //使用默认实参
成员模板
成员模板不能是虚函数*****类模板的成员模板*****
template<typename T> class A
{template<typename K> A(K k);
};template<typename T> //类的类型参数
template<typename K> //构造函数的类型参数
A<T>::A(K k){} //定义
实例化与成员模板
为了实例化一个类模板的成员模板,必须同时提供类和函数模板的实参。
string s="abc";
A<int> a(s); //实例化版本:A<int>::A(string);
A<float> a2(666); //实例化版本:A<float>::A(int);
控制实例化
当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个文件中,当两个或多个独立编译的源文件使用了相同的模板
并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大型系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,通过显示实例化避免这种开销:
extern template declaration; //实例化声明
template declaration; //实例化定义
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。例如:
extern template class A<int>; //声明
template int compare(const int&,const int&); //定义当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序的其他位置有该
实例化的一个非extern声明(定义),对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前:
//B.cpp
//这些模板必须在程序其他位置进行实例化
extern template class A<int>;
extern template int compare(const int&,const int&);
A<int> a1,a2; //实例化出现在其他位置
int i = compare(1,2); //实例化出现在其他位置当编译器遇到一个实例化定义时,为其生成代码。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
效率和灵活性
2.模板实参推断
函数模板显示实参
template<typename T1,typename T2,typename T3>
T1 sum(T2,T3);auto val = sum<long long>(3,8);
T1是显示指定的:long long
T2和T3是推断出来:int
显示模板实参按由左至右的顺序与对应的模板参数匹配
位置返回类型与类型转换
template<typename T>
??? &fun(T beg,T end) //不知道返回结果的准确类型
{ //处理return *beg;
}vector<int> vec;
auto &v = fun(vec.begin(),vec.end()); //返回类型是int&我们知道函数应该返回*beg,而且可以用decltype(*beg)来获取此表达式的类型。
但是,在编译器遇到函数的参数列表之前,beg是不存在的。为了定义此函数,必须使用尾置返回类型。
由于尾置返回出现在参数列表之后,可以使用函数的参数:
template<typename T>
auto fun(T beg,T end) -> decltype(*beg) //解引用后是一个左值,迭代器生成元素的引用,所以decltype()表示元素类型的引用
{//处理return *beg;
}
进行类型转换的标准库模板类
有时无法直接获得所需要的类型。例如,我们可能希望编写一个类似上面fun的函数,但返回一个元素的值而非引用。
template<typename T>
??? fun(T beg,T end) //要返回一个元素的值
{ //处理return *beg;
}面临一个问题:对于传递的参数的类型,我们几乎一无所知。在此函数中,唯一可以使用的操作时迭代器操作,而所有迭代器操作
都不会生成元素,只能生成元素的引用。
为了获得元素类型,可以使用标准库的类型转换模板。这个模板定义在头文件type_traits中。这个头文件中的类通常用于所谓的
模板元程序设计,不再本书的范围。但是,类型转换模板在普通编程中也很有用。后面会看到它们是如何实现的。本例中,我们可以使用remove_reference来获得元素类型。就如英文名的意思:去引用
remove_reference<decltype(*beg)>::type
decltype(*beg)返回的是引用类型
remove_reference::type脱去引用,剩下元素类型本身。组合使用remove_reference、尾置返回即decltype,可以在函数中返回元素值的拷贝:
template<typename T>
auto fun(T beg, T end)->typename remove_reference<decltype(*beg)>::type
{//处理return *beg;
}
注意,type是一个类的成员,而该类依赖于一个模板参数,因此,必须在返回类型的声明中使用typename来告知编译器,type表示一个类型
举例:
int a = 1;
int& b = a;
int&& c = 1;
decltype(a) t1 = 1; t1的类型是int
decltype(b) t2 = a; t2的类型是int&
decltype(c) t3 = 1; t3的类型是int&&
remove_reference<decltype(a)>::type d = 3; d、e、f去除引用的类型都是int
remove_reference<decltype(b)>::type e = 3;
remove_reference<decltype(c)>::type f = 3;同理:
add_const<decltype(a)>::type 类型是const int
add_const<decltype(b)>::type 类型是int&
add_const<decltype(c)>::type 类型是int&&
函数指针和实参推断
模板实参推断和引用
转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,需要保持被转发实参的所有性质,包括实参类型
是否是const的以及实参时左值还是右值。例如:flip1是一个不完整的实现:顶层const和引用丢失了
template<typename F,typename T1,typename T2>
void flip1(F f,T1 t1,T2 t2)
{f(t2,t1);
}
这个函数一般情况下工作得很好,但当我们希望用它调用一个接受引用参数的函数时就会出现问题:
void f(int v1,int &v2)
{cout<<v1<<" "<<++v2<<endl;
}
在这段代码中,f改变了绑定到v2实参的值。但是,如果我们通过flip1调用f,f所做的改变就不会影响实参:
f(3,i);
flip1(f,j,3); //j被拷贝后,传递给f的t1是一个副本,不会影响j*****定义能保持类型信息的函数参数*****
为了通过翻转函数传递一个引用,我们需要重新函数,使其参数能保持给定实参的“左值性”。更进一步,也希望保持参数的const属性
通过将一个函数参数定义为一个指向模板类型参数的右值引用,可以保持其对应实参的所有类型信息。而使用引用参数(无论左值还是
右值)使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T1&&和T2&&,通过引用折叠就
可以保持翻转实参的左值/右值属性。
template<typename F,typename T1,typename T2>
void flip2(F f,T1 &&t1,T2 &&t2)
{f(t2,t1);
}
与较早的版本一样,如果我们调用flip2(f,j,3),将传递给参数t1一个左值j。但是,在flip2中,推断出的T1的类型为int&,这意味着
t1的类型会折叠为int&。由于是引用类型,t1被绑定到j上。当flip2调用f时,f中的引用参数v2被绑定到t1,也就是被绑定到j。当f
递增v2时,它也同时改变了j的值。注:如果一个函数参数的指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左值/右值属性将得到保持。这个版本的flip2解决了一半问题。它对于接受一个左值引用的函数工作得很好,但不能用于接受右值引用参数的函数。例如:
void g(int &&i,int & j)
{cout<<i<<" "<<j<<endl;
}
flip2(g,i,3); //不能从一个左值实例化int&&
传递给g的将是flip2中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式。因此flip2中对g的调用将传递给g的右值引用
参数一个左值*****在调用中使用std::forward保存类型信息*****
可以使用一个名为forward的新标准库设施来传递flip2的参数,它能保持原始实参的类型。类似move,forward也定义在头文件utility
中,与move不同,forward必须通过显示模板实参来调用。forward返回该显示实参类型的右值引用:即,forward<T>的返回类型是T&&通常情况下,使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定
实参的左值右值属性:
template<typename Type> intermediary(Type &&arg)
{finalFcn(std::forward<Type>(arg);
}
由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。
如果实参是一个右值,则Type是一个普通(非引用)类型,forward<T>将返回Type&&。
如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型,在此情况下,返回类型是一个指向左值引用类型的右值引用。
再次对forward<Type>的返回类型进行阴影折叠,将返回一个左值引用类型。使用forward,可以再次重写翻转函数:
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(g,i,3),i将以int&类传递给g,3将以int&&类型传递给g。
3.重载与模板
看书理解下就可以了
当有多个重载模板对一个调用提供同样好的匹配时,应该选择最特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。通常,如果使用了一个忘记声明的函数,代码将编译失败。但对于重载函数模板的函数而言,则不是这样。
如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。
4.可变参数模板
template<typename T,typename... Args> //Args是一个模板参数包,表示0个或多个模板类型参数
void fun(const T& t,const Args& ... rest); //rest是一个函数参数包,表示0个或多个函数参数fun(1,"hi",3,14,3); //自动推断类型******sizeof...运算符*****
获取包中有多少元素
template<typename ... Args> void g(Args ... args)
{cout<<sizeof...(Args)<<endl; //类型参数的数目cout<<sizeof...(args)<<endl; //函数参数的数目
}*****编写可变参数函数模板*****
可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
template<typename T>
ostream& print(ostream &os,const T &t)
{return os<<t; //包中最后一个元素之后不打印分隔符
}template<typename T,typename... Args>
ostream& print(ostream& os,const T& t,const Args&... rest)
{os<<t<<", "; //打印第一个实参return print(os,rest...);
}第一个版本的print负责终止递归并打印调用中的最后一个实参。
第二个版本的print是可变参数版本,它打印绑定到t的实参,并调用自身来打印函数参数包中的剩余值。注:当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无须递归。*****包扩展*****
template<typename T,typename... Args>
ostream& print(ostream& os,const T& t,const Args&... rest) //扩展Args
{os<<t<<", ";return print(os,rest...); //扩展rest
}
第一个扩展操作扩展模板参数包,为print生成函数参数列表。
第二个扩展操作为print调用生成实参列表
例如:
int i=1;
string s="hi";
print(cout,i,s,3); //包中有两个参数
上面的print调用被实例化为:
ostream& print(ostream&,const int&,const string&,const int&);*****理解包扩展*****
print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式。
template<typename... Args>
ostream& errorMsg(ostream& os,const Args&... rest)
{return print(os,debug(rest)...); 对每个实参执行debug//等价 print(os,debug(a1),debug(a2),...,debug(an));
}
扩展中的模式会独立地应用于包中每个元素*****转发参数包*****
在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。例如:
为了保持实参中的类型信息,必须将emplace_back的函数参数定义为模板类型参数的右值引用。
class StrVec
{public:template<class... Args> void emplace_back(Args&&...);//其他成员
};
模板参数包扩展中的模式是&&,意味着每个函数参数将是一个指向其对应实参的右值引用。
其次,当emplace_back将这些实参传递给construct时,必须使用forward来保持实参的原始类型:
template<class...Args>
inline
void StrVec::emplace_back(Args&&... args)
{chk_n_alloc(); //如果需要的话重新分配StrVec内存空间alloc.construct(first_free++,std::forward<Args>(args)...); //即扩展了模板参数包Args,也扩展了函数参数包args
}如果调用:
svec.emplace_back(4,'c'); //将cccc添加为新的尾元素
construct调用中的模式会扩展出:
std::forward<int>(10),std::forward<char>(c)
5.模板特例化
C++ Primer学习笔记-----第十六章:模板与泛型编程相关推荐
- [go学习笔记.第十六章.TCP编程] 3.项目-海量用户即时通讯系统-redis介入,用户登录,注册
1.实现功能-完成用户登录 在redis手动添加测试用户,并画出示意图以及说明注意事项(后续通过程序注册用户) 如:输入用户名和密码,如果在redis中存在并正确,则登录,否则退出系统,并给出相应提示 ...
- C++ Primer plus学习笔记-第十六章:string类和标准模板库
第十六章:string类和标准模板库 前言:这一章已经相当靠近全书的后面部分了:这一章我们会深入探讨一些技术上的细节,比如string的具体构造函数,比如适用于string类的几个函数,比如我们还会介 ...
- [汇编学习笔记][第十六章直接定址表]
第十六章 直接定址表 16.1 描述了单元长度的标号 格式 code segmenta db 1,2,3,4,5,6,7,8,b dw 0 功能 此时标号a,b 不仅代表了内存单元,还代表了内存长度 ...
- 深入立即Linux网络技术内幕学习笔记第十六章:桥接:Linux实现
网桥设备抽象: 对Linux而言,网桥是虚拟设备,要想传输或接收数据,需要将真实设备绑定到虚拟网桥上. 上图中,有几点需要注意: LAN1和LAN2通过网桥连接在一起,子网都是一样的. 网桥连接到路由 ...
- C++Primer Plus笔记——第十六章 string类和标准模板库总结及程序清单
目录 本章小结 程序清单 string类 16.1 str1.cpp 16.2 strfile.cpp 16.3 hangman.cpp ...
- UNIX环境高级编程 学习笔记 第十六章 网络IPC:套接字
socket的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信.socket接口可采用许多不同的网络协议进行通信,本章讨论限制在因特网事实上的通信标准:TCP/IP协议栈. 套接 ...
- TCP/IP详解 卷1:协议 学习笔记 第十六章 BOOTP:引导程序协议
一个无盘系统在不知道自身IP地址情况下,进行系统引导时能通过RARP协议获取它的IP地址,使用RARP会有两个问题:(1)IP地址是返回的唯一结果:(2)RARP使用链路层广播,RARP请求不会被路由 ...
- Linux shell编程学习笔记-----第十六章
shell 脚本调试技术:trap命令 tee命令 调试钩子 和shell选项.前三者都需要修改shell脚本的源代码,后者不需要. trap 命令用于捕捉信号,shell脚本在执行时,会产生三个所 ...
- 《VC++深入详解》学习笔记 第十六章 线程同步与异步套接字编程
(颠簸喜悲幽若尽是无情人) 事件对象成员: 包含(使用计数.事件类型.事件状态) 事件类型: 人工重置的事件对象:得到通知时,等待的所有线程都变为可调度 自动重置的事件对象:得到通知时,等待的 ...
最新文章
- thymeleaf 的 th:each简单应用
- python报错UnicodeDecodeError: ‘ascii‘ codec can‘t decode byte 0xe8 in position 0 解决方案
- 网络营销外包专员浅析尽管快照不见了网络营销外包仍在继续
- 软件项目管理0720:读一页项目管理-项目子目标
- music‘s effects
- 打趴系统的不一定是技术
- Spring-core-AnnotationMetadata接口
- 找到问题比解决问题更重要
- 在ubuntu 下安装基于 Tomcat6的web服务
- 如何学习Python进行数据分析
- MySQL binlog后面的编号最大是多大?【老叶茶馆公众号】
- Vue:错误Component template should contain exactly one root element解决
- node js unknown option -v._mac os上搭建node环境(nvm, node.js, npm)
- 01-hadoop学习环境准备
- 『Windows Builder』Java Swing期末课设神器
- 一次编写命令时遇到的问题,Ambiguous method call.both
- 月饼电商“内卷”?看数据如何驱动营销,全链路精细化运营抢占C位!
- python怎么算一元二次方程_Python求一元二次方程解
- 防范勒索软件的分层办法
- 扇贝上的python靠谱吗_我终于找到了扇贝的秘密