Chapter16:模板
- 函数模板
编译器通常用函数实参来为我们推断模板实参。
编译器用推断出的模板参数来为我们实例化(instantiate)一个特定版本的函数。生成的版本通常被称为模板的实例。
我们可以在模板中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。模板实参必须是常量表达式,从而允许编译器在编译时实例化模板。(绑定到非类型整数参数的实参必须是常量表达式;绑定到指针或引用非类型参数的实参必须具有静态的生存期;我们不能用一个普通的局部变量或动态对象作为指针或引用非参数模板参数的实参。)
inline或constexpr的函数模板:
1 //正确 2 template<typename> 3 inline T min(const T&, const T&); 4 //错误 5 inline template<typename>T min(const T&, const T&);
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当实例化一个模板时,编译器才生成代码。
当我们使用模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
通常来说,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类的定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放到源文件中;
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
- 类模板
与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。
为了阅读模板类代码,应该记住类模板的名字不是一个类型名。
对于一个实例化了的类模板,成员函数只有在被用到时才进行实例化。
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,指导遇到类名才表示进入类的作用域:
1 template<typename T> class BlobPtr 2 { 3 BlobPtr& operator++();//不需要模板参数 4 }; 5 6 template<typename T> 7 BlobPtr<T> BlobPtr<T>::operator++(int)//需要模板参数 8 { 9 BlobPtr ret = *this; //不需要模板参数 10 ++*this; 11 return ret; 12 }
类模板和友元:
情况一:一对一
1 //为了引用模板的一个特定实例,我们必须首先声明模板自身 2 template<typename> class BlobPtr; 3 template<typename> class Blob; 4 template<typename T> bool operator==(const Blob<T>&, const Blob<T>&); 5 6 template<typename T> class Blob 7 { 8 friend class BlobPtr<T>; 9 friend bool operator==<T>(const Blob<T>&, const Blob<T>&); 10 11 };
情况二:
1 //因为只需要Pal的特定实例;Pal2所有实例都需要,所以只有Pal前置声明 2 template<typename T> class Pal; 3 class C 4 { 5 friend class Pal<C>;//Pal<C>是友元 6 template<typename T> friend class Pal2;//Pal2所有实例都是C的友元 7 }; 8 9 template<typename T>class C2 10 { 11 friend class Pal<T>; 12 template<typename X> friend class Pal2; 13 //Pal3是一个非模板类,它是C2所有实例的友元 14 friend class Pal3; 15 16 };
情况三:新标准中可以将模板类型参数声明为友元
1 template<typename Type> class Bar 2 { 3 friend Type; 4 };
- 模板参数
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。与任何其它的名字一样,模板参数会隐藏外层作用域中声明的相同名字。
一个问题:我们使用作用域运算符(::)来访问static成员和类型成员,在普通代码中,编译器掌握类的定义,所以可以知道要访问的名字是类型还是static成员;但是对于模板代码,T::mem,编译器不知道mem是一个类型还是数据成员,所以必须显式指定。
默认情况下,C++假定通过作用域运算符访问的名字不是类型,如果要访问类型,需要typename显式指定,那么:
1 template<typename T> 2 typename T::value_type top() 3 { 4 }
- 控制实例化
当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显示实例化来避免这种开销。
1 extern template declaration;//实例化声明 2 template declaration;//实例化定义
1 extern template class Blob<string>;//声明 2 template int compare(const int&, const int&);//定义
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动化对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。因为编译器不知道程序会使用哪些成员函数。
- 模板实参推断
只允许两种转换:
1. const转换:可以将一个非const对象的引用传递给一个const的引用形参;
2. 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
其他类型转换,如算术转换、派生类向基类转换,以及用户定义的转换,都不能应用于函数模板。
但是如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
同时,对于模板类型参数已经显式指定了的函数实参,也可以进行正常的类型转换。
对于这种情况怎么办?
1 template<typename It> 2 ? ? ? &fcn(It beg, It end) 3 { 4 return *beg; 5 }
1 template<typename It> 2 auto fcn(It beg, It end) ->decltype(*beg) 3 { 4 return *beg; 5 }
- 模板实参推断和引用
1 template<typename T> void f1(T&); 2 f1(i);//i是int,T推断为int 3 f1(ci);//ci是const int,T为const int 4 f1(5);//错误,传递给&参数的必须是左值,(因为const可以转换,所以不能推断出const来) 5 6 template<typename T> void f2(const T&); 7 f2(i);//i是int,T推断为int 8 f2(ci);//ci是const int,T为int 9 f2(5);//T为int 10 11 template<typename T> void f3(T&&); 12 f3(42);//T为int
f3(i)这样的调用时不合法的。但是在C++中,正常的绑定规则之外,有两个例外,这是move设施正确工作的基础。
1. 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型;即:f3(i),T为int&。
2. 如果我们间接创建了引用的引用,那么除了右值引用的右值引用之外,其他都折叠为左值引用。
由此:我们可以把任意类型的实参传递给T&&类型的函数参数。
但是,当代吗涉及的类型可能是普通(非引用)类型,也可能是引用类型时,编写正确的代码就异常困难,所以,在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。
std::move
如何将左值转换为右值?可以使用static_cast显式地将一个左值转换为一个右值。但是,最好使用move
template<typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
转发
转发需要保证实参的所有性质:实参类型是否是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; } flip1(f, j, 42);//T1 推断为int,不符合预期
如何解决?答案是使用右值引用。
template<typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) { f(t2, t1); }
但是,还有一个问题,F函数如果参数是左值引用,可以工作的很好;如果是右值引用,则出现错误。
void g(int &&v1, int& v2) { cout << v1 << " " << ++v2 << endl; }
flip2(g,i,42);
//t2是类型为右值引用的变量,作为左值,不能传递给右值引用
如何解决这一问题?forwaed<T>返回类型是T&&
1 template<typename F, typename T1, typename T2> 2 void flip(F f, T1 &&t1, T2 &&t2) 3 { 4 f(std::forward<T2>(t2), std::forward<T1>(t1)); 5 }
如果实参是右值,那么Type就是普通类型,forward就返回Type&&,还是右值;
如果实参是左值,那么Type就是左值引用类型,forward就返回左值引用的右值引用,折叠之后是左值引用。
所以:右值引用形参+forward完成了转发工作。
- 模板重载的几条规则:
如果同样好的函数中只有一个是非模板函数,则选择此函数;
如果同样好的函数中没有非模板参数,而有很多函数模板,且其中一个模板比其他模板更特例化,则选择此模板。(没有他的话,本质上(const T&)可以用于任何类型);
- 可变参数模板:
参数包:模板参数包和函数参数包;
当我们需要知道包中有多少元素时,可以使用sizeof...()运算符。
可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余的实参调用本身。
template<typename T> osteram &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...); }
包扩展:模式。
一个应用:转发参数包
class StrVec { public: template<class...Args>void emplace_back(Args&&...) { alloc.construct(first_free++, std::forward<Args>(args)...); } };
- 模板特例化
在某些情况下,通用模板的定义是不合适的。需要特例化。
函数模板特例化
template<> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); }
注意:一个特例化版本本质上是一个实例,而非函数的重载版本,因此,不影响函数匹配。
类模板可以部分特例化:只指定部分参数;或者参数的部分特性(例如引用)。
可以特例化成员而不是类。