16.1 定义模板
模板是C++泛型编程的基础。为模板提供足够的信息,就能生成特定的类或函数。
16.1.1 函数模板
在模板定义中,模板参数列表不能为空。
template < typename T >
int compare ( const T & v1, const T & v2)
{
if ( v1 < v2) return - 1 ;
if ( v2 < v1) return 1 ;
return 0 ;
}
当调用一个函数模板时,编译器(通常)用函数实参来推断模板实参,然后实例化一个特定版本的函数。
cout << compare ( 1 , 0 ) << endl;
vector< int > vec1{ 1 , 2 , 3 } , vec2{ 4 , 5 , 6 } ;
cout << compare ( vec1, vec2) << endl;
类型参数前必须使用关键字class或typename。在模板参数列表中,这两个关键字的含义相同,可以互换使用。 类型参数可以用来指定返回类型或函数的参数类型。
template < typename T > T foo ( T* p)
{
T tmp = * p;
return tmp;
}
template < typename T , U> T calc ( const T& , const U& ) ;
template < typename T , class U > calc ( const T& , const U& ) ;
可以在模板中定义非类型参数,表示一个值而非一个类型。
template < unsigned N, unsigned M>
int compare ( const char ( & p1) [ N] , const char ( & p2) [ M] )
{
return strcmp ( p1, p2) ;
}
compare ( "hi" , "mom" ) ;
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。 一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。 非类型模板参数的模板实参必须是常量表达式。 编写泛型代码的两个重要原则: 模板中的函数参数是const的引用。(保证函数可以用于不能拷贝的类型) 函数体中的条件判断仅使用<比较运算。(降低函数对要处理的类型的要求) 模板程序应该尽量减少对实参类型的要求。
template < typename T > int compare ( const T & v1, const T & v2) {
if ( v1 < v2) return - 1 ;
if ( v2 < v1) return 1 ;
return 0 ;
}
template < typename T > int compare ( const T & v1, const T & v2) {
if ( less < T> ( ) ( v1, v2 ) ) return - 1 ;
if ( less < T> ( ) ( v1, v2 ) ) return 1 ;
return 0 ;
}
函数模板和类模板成员函数的定义通常放在头文件中。 当编写模板时,代码不能是针对特定类型的,但模板代码通常对其所使用的类型有一些假设。 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
16.1.2 类模板
编译器不能为类模板推断模板参数类型。 一个类模板的每个实例都形成一个独立的类。 类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
Blob< int > squares = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;
for ( size_t i = 0 ; i!= squares. size ( ) ; ++ i)
squares[ i] = i* i;
在类模板自己的作用域中,可以直接使用模板名而不提供实参。
template < typename T >
BlobPtr< T> BlobPtr< T> :: operator ++ ( int )
{
BlobPtr ret = * this ;
++ * this ;
return ret;
}
C++11允许为类模板定义一个类型别名。
template < typename T > using twin = pair< T, T> ;
twin< string> authors;
twin< int > win_loss;
twin< double > area;
template < typename T > using partNo = pair< T, unsigned > ;
partNo< string> book;
partNo< Vehicle> cars;
partNo< Student> kids;
类模板和友元:一对一友好关系。
template < typename > class BlobPtr ;
template < typename > class Blob ;
template < typename T >
bool operator == ( const Blob< T> & , const Blob< T> & ) ;
template < typename T > class Blob {
friend class BlobPtr < T> ;
friend bool operator == < T> ( const Blob< T> & , const Blob< T> & ) ;
} ;
Blob< char > ca;
Blob< int > ia;
为了让所有实例成为友元,友元声明中必须使用与模板本身不同的模板参数。
template < typename T > class Pal ;
class C {
friend class Pal < C> ;
template < typename T > friend class Pal2 ;
} ;
template < typename T > class C2 {
friend class Pal < T> ;
template < typename X > friend class Pal2 ;
friend class Pal3 ;
} ;
令模板自己的类型参数成为友元。
template < typename Type > class Bar {
friend Type;
} ;
类模板的static成员。
template < typename T > class Foo {
public :
static std:: size_t count ( ) { return ctr; }
private :
static std:: size_t ctr;
} ;
Foo< string> fs;
Foo< int > fi, fi2, fi3;
template < typename T >
size_t Foo< T> :: ctr = 0 ;
Foo< int > fi;
auto ct = Foo < int > :: count ( ) ;
ct = fi. count ( ) ;
ct = Foo :: count ( ) ;
16.1.3 模板参数
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次。
typedef double A;
template < typename A , typename B > void f ( A a, B b) {
A tmp = a;
double B;
}
template < typename V , typename V >
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。 默认情况下,C++假定通过作用域运算符访问的名字不是类型。 当希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class。 C++11可以为函数和类模板提供默认实参。
template < class T = int > class Numbers {
public :
Numbers ( T v= 0 ) : val ( v) { }
private :
T val;
} ;
Numbers< long double > lots_of_precision;
Numbers< > average_precision;
与函数参数相同,声明中的模板参数的名字不必与定义中相同。
template < typename T > T calc ( const T& , const T& ) ;
template < typename U > U calc ( const U& , const U& ) ;
template < typename Type >
Type calc ( const Type& a, const Type& b) { }
16.1.4 成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
class DebugDelete {
public :
DebugDelete ( std:: ostream & s = std:: cerr) : os ( s) { }
template < typename T > void operator ( ) ( T* p) const
{ os << "deleting unique_ptr" << std:: endl; delete p; }
private :
std:: ostream & os;
} ;
double * p = new double ;
DebugDelete d;
d ( p) ;
int * ip = new int ;
DebugDelete ( ) ( ip) ;
unique_ptr< int , DebugDelete> p ( new int , DebugDelete ( ) ) ;
unique_ptr< string, DebugDelete> sp ( new string, DebugDelete ( ) ) ;
void DebugDelete :: operator ( ) ( int * p) const { delete p; }
void DebugDelete :: operator ( ) ( string * p) const { delete p; }
16.1.5 控制实例化
通过显式实例化来避免在多个文件中实例化相同模板的额外开销。
extern template class Blob < string> ;
template int compare ( const int & , const int & ) ;
extern template class Blob < string> ;
extern template int compare ( const int & , const int & ) ;
Blob< string> sa1, sa2;
Blob< int > a1 = { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } ;
Blob< int > a2 ( a1) ;
int i = compare ( a1[ 0 ] , a2[ 0 ] ) ) ;
template int compare ( const int & , const int & ) ;
template class Blob < string> ;
将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。 由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。 对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。 在一个类模板的实例化定义中,所有类型必须能用于模板的所有成员函数。
16.1.6 效率与灵活性
del? del ( p) : delete p;
del ( p) ;
16.2 模板实参推断
16.2.1 类型转换与模板类型参数
编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。 在其他类型转换中,能在调用中应用于函数模板的包括如下两项: 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" ) ;
fobj ( s1, s2) ;
fref ( s1, s2) ;
int a[ 10 ] , b[ 42 ] ;
fobj ( a, b) ;
fref ( a, b) ;
使用相同模板参数类型的函数形参。
long lng;
compare ( lng, 1024 ) ;
template < typename A , typename B >
int flexibleCompare ( const A& v1, const B& v2)
{
if ( v1< v2) return - 1 ;
if ( v2< v1) return 1 ;
return 0 ;
}
long lng;
flexibleCompare ( lng, 1024 ) ;
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
template < typename T > ostream & print ( ostream & os, const T & obj)
{
return os << obj;
}
print ( cout, 42 ) ;
ofstream f ( "output" ) ;
print ( f, 10 ) ;
16.2.2 函数模板显式实参
只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
template < typename T1 , typename T2 , typename T3 >
T1 sum ( T2, T3) ;
auto val3 = sum < long long > ( 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) ;
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。
long lng;
compare ( lng, 1024 ) ;
compare < long > ( lng, 1024 ) ;
compare < int > ( lng, 1024 ) ;
16.2.3 尾置返回类型与类型转换
template < typename It >
? ? ? & fcn ( It beg, It end) {
return * beg;
}
vector< int > vi = { 1 , 2 , 3 , 4 , 5 } ;
Blob< string> ca = { "hi" , "bye" } ;
auto & i = fcn ( vi. begin ( ) , vi. end ( ) ) ;
auto & s = fnc ( ca. begin ( ) , ca. end ( ) ) ;
template < typename It >
auto fcn ( It beg, It end) -> decltype ( * beg) {
return * beg;
}
组合使用remove_reference、尾置返回及decltype,就可以在函数中返回元素值的拷贝。
template < typename It >
auto fcn2 ( It beg, It end) -> typename remove_reference < decltype ( * beg) > :: type
{
return * beg;
}
16.2.4 函数指针和实参推断
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
template < typename T > int compare ( const T& , const T& ) ;
int ( * pf1) ( const int & , const int & ) = compare;
void func ( int ( * ) ( const string& , const string& ) ) ;
void func ( int ( * ) ( const int & , const int & ) ) ;
func ( compare) ;
func ( compare< int > ) ;
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
16.2.5 模板实参推断和引用
只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。 如果一个函数参数是指向模板参数类型的右值引用(如T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。 在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。 左值传递给函数的右值引用参数(模板类型参数),编译器推断模板类型参数为实参的左值应用类型。
template < typename T > void f1 ( T& ) ;
f1 ( i) ;
f1 ( ci) ;
f1 ( 5 ) ;
template < typename T > void f2 ( const T& ) ;
f2 ( i) ;
f2 ( ci) ;
f2 ( 5 ) ;
16.2.6 理解std::move
标准库move函数是使用右值引用的模板的一个很好的例子。
template < typename T >
typename remove_reference < T> :: type&& move ( T&& t)
{
return static_cast < typename remove_reference < T> :: type&& > ( t) ;
}
string s1 ( "hi" ) , s2;
s2 = std:: move ( string ( "bye!" ) ) ;
s2 = std:: move ( s1) ;
C++11 :可以用static_cast显式地将一个左值转换为一个右值引用。
16.2.7 转发
如果一个函数参数是指向模板类型参数的右值引用(如 T&&),它对应的实参的const属性和左值/右值属性将得到保持。 C++11:当用于一个指向模板参数类型的右值引用函数参数(T&&)时,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, 42 ) ;
与std::move相同,对于std::forward不使用using声明是一个好主意。
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 ( 42 , i) ;
flip1 ( f, j, 42 ) ;
template < typename F , typename T1 , typename T2 >
void flip2 ( F f, T1 && t1, T2 && t2)
{
f ( t2, t1) ;
}
void g ( int && i, int & j)
{
cout<< i<< " " << j<< endl;
}
flip2 ( f, j, 42 ) ;
flip2 ( g, i, 42 ) ;
16.3 重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。
cout << debug_rep ( "hi world!" ) << endl;
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
template < typename T > string debug_rep ( const T & t) {
ostringstream ret;
ret << t;
return ret. str ( ) ;
}
template < typename T > string debug_rep ( T * p) {
ostringstream ret;
ret << "pointer: " << p;
if ( p)
ret << " " << debug_rep ( * p) ;
else
ret << " null pointer" ;
return ret. str ( ) ;
}
int main ( ) {
string s ( "hi" ) ;
cout << debug_rep ( s) << endl ;
cout << debug_rep ( & s) << endl;
const string * sp = & s;
cout << debug_rep ( sp) << endl;
}
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
string debug_rep ( const string & s) {
return '"' + s + '"' ;
}
string s ( "hi" ) ;
cout << debug_rep ( s) << endl;
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到希望调用的函数而实例化一个并非所需的版本。
template < typename T > string debug_rep ( const T & t) ;
template < typename T > string debug_rep ( T * p) ;
string debug_rep ( const string& ) ;
string debug_rep ( char * p) {
return debug_rep ( string ( p) ) ;
}
16.4 可变参数模板
在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
template < typename T , typename . . . Args>
void foo ( const T & t, const Args& . . . rest) ;
int i = 0 ; double d = 3.14 ; string s = "how now brown cow" ;
foo ( i, s, 42 , d) ;
foo ( s, 42 , "hi" ) ;
foo ( d, s) ;
foo ( "hi" ) ;
void foo ( const int & , const string& , const int & , const double & ) ;
void foo ( const string& , const int & , const char [ 3 ] & ) ;
void foo ( const double & , const string& ) ;
void foo ( const char [ 3 ] & ) ;
当需要知道包中有多少元素时,可以使用sizeof…运算符。
template < typename . . . Args> void g ( Args . . . args) {
cout<< sizeof . . . ( Args) << endl;
cout<< sizeof . . . ( args) << endl;
}
16.4.1编写可变参数函数模板
当定义可变参数版本的printf时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。
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 ( cout, i, s, 42 ) ;
16.4.2 包扩展
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边放一个省略号来触发扩展操作。 扩展中的模式会独立地应用于包中的每个元素。
template < typename . . . Args>
ostream & errorMsg ( ostream & os, const Args& . . . rest)
{
return print ( os, debug_rep ( rest) . . . ) ;
}
errorMsg ( cerr, fncName, code, num ( ) , otherData, "other" , item) ;
print ( os, debug_rep ( rest. . . ) ) ;
print ( cerr, debug_rep ( fcnName, code. num ( ) , otherData, "otherData" , item) ) ;
16.4.3 转发参数包
C++11 :可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。
class StrVec {
public :
template < class . . . Args> void emplace_back ( Args&& . . . ) ;
} ;
template < class . . . Args>
inline
void StrVec :: emplace_back ( Args&& . . . args)
{
chk_n_alloc ( ) ;
alloc. construct ( first_free++ , std:: forward < Args> ( args) . . . ) ;
}
svec. emplace_back ( 10 , 'c' ) ;
svec. emplace_back ( s1+ s2) ;
16.5 模板特例化
一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。 特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参,使用关键字template后跟一个空尖括号对,指出正在实例化一个模板。
template < >
int compare ( const char * const & p1, const char * const & p2)
{
return strcmp ( p1, p2) ;
}
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
compare ( "hi" , "mom" ) ;
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。 一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。 只能部分特例化类模板,而不能部分特例化函数模板。
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;
int & ri= i;
remove_reference< decltype ( 42 ) > :: type a;
remove_reference< decltype ( ri) > :: type b;
remove_reference< decltype ( std:: move ( i) ) > :: type c;
标准库算法都是函数模板,标准库容器都是类模板。 当我们不能(或不希望)使用模板版本时,可以定义一个特例化版本。
template < typename T > int compare ( const T& , const T& ) ;
template < size_t N, size_t M>
int compare ( const char ( & ) [ N] , const char ( & ) [ M] ) ;
const char * p1 = "hi" , * p2 = "mom" ;
compare ( p1, p2) ;
compare ( "hi" , "mom" ) ;