第十六章 定义模板 1 2 3 4 5 6 7 int compare (const string& v1, const string& v2) { if (vl < v2) return -l; if (v2 < v1) return 1 ; return 0 ; } int compare (const double & v1, const double & v2) { if (v1 < v2) return -1 ; if (v2 < v1) return 1 ; return 0 ; }
对于像这样除了类型意外一模一样的函数,我们可以使用模板来适配各种各样的类型。
函数模板 我们可以定义一个通用的函数模板,而不是为每一个类型定义一个新的函数。则compare的模板可能像下面这样:
1 2 3 4 5 6 template <typename T>int compare (const T& v1, const T& v2) { if (v1 < v2) return -1 ; if (v2 < vl) return 1 ; return 0 ; }
模板定义以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个逗号分隔的一个或多个模板参数(template parameter)的列表,用小于号(<)和大于号(>)包围起来。
在模板定义中,模板参数列表不能为空。
实例化函数模板 调用一个函数模板时,编译器用函数的实参来推断模板实参。
1 cout <<compare (1 ,0 )<< endl;
编译会推断出实参int,并绑定到T,这样推断出来的为我们实例化 一个特定的函数。
1 2 3 4 5 cout << compare (1 ,0 ) << endl; vector<int > vec1{1 ,2 ,3 }, vec2{4 ,5 ,6 }; cout << compare (vec1, vec2) <<endl;
这里编译器会实例化两个不同版本的compare。其中一个T为int,另一个T为vector,编译器生成的版本成为模板的实例。
模板类型参数 我们的compare函数有一个模板类型参数(type parameter)。一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:
1 2 3 4 5 template <typename T> Tfoo (T* p){ T tmp = *p; return tmp; }
类型参数前必须使用关键字class或typename:
1 2 3 4 template <typename T, U> T calc (const T&,const U&) ;template <typename T, class U > calc (const T&,const U&) ;
因为可以使用非类的类型作为模板实参,所以使用typename更为直观。
非类型模板参数 除了模板参数,我们还可以定义非类型参数,简言之就是一个固定的值,当模板被实例化后,非类型模板参数就被这个值所取代,这个值也必须时常量表达式,例如:
1 2 3 4 template <unsigned N, unsigned M>int compare (const char (&p1)[N], const char (&p2)[M]) { return strcmp (p1, p2) ; }
当调用时:
最后编译器使用字面常量大小代替N和M:
1 int compare (const char (&p1) [3 ],const char (&p2)[4 ])
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
通常在需要常量表达式的地方需要用到此参数。
inline和constexpr的函数模板 1 2 3 4 template <typename T> inline T min (const T&,const T&) ;inline template <typename T> T min (const T&,const T&) ;
我们最初的compare函数虽然简单,但它说明了编写泛型代码的两个重要原则:
模板中的函数参数是const的引用。
函数体中的条件判断仅使用<比较运算。
通过const+引用的方式,我们保证了函数可以用于不能拷贝的类型。
此外,我们没必要即使用<
又使用>
运算符。
1 2 3 4 5 6 7 8 if (v1 < v2) return -l;if (v1 > v2) return l;return 0 ;template <typename T> int compare (const T &v1,const T &v2) if ( less<T>()(vl, v2)) return -1 ;if ( less<T>() (v2, v1)) return l;return 0 ;
原始版本存在的问题是,如果用户调用它比较两个指针,且两个指针未指向相同的数组,则代码的行为是未定义的(据查阅资料,less的默认实现用的就是<,所以这其实并未起到让这种比较有一个良好定义的作用—译者注)。
模板程序应该尽量减少对实参类型的要求。
模板编译 编译器遇到模板时,只有当实例化除模板特例时,才会生成代码。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
关键概念:模板和头文件
模板包含两种名字:
那些不依赖于模板参数的名字
那些依赖于模板参数的名字
当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。
用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。
通过组织良好的程序结构,恰当使用头文件,这些要求都很容易满足。模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
实例化器件错误报告 模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报告错误。
第一个阶段是编译模板本身时。在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分号或者变量名拼错等,但也就这么多了。
第二个阶段是编译器遇到模板使用时。在此阶段,编译器仍然没有很多可检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。它还能检查参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
例如原始版本的if (vl < v2) return -l;//要求类型T的对象支持<操作
其中如果调用者传入类型没有<
运算符,则会在第三个阶段报错。
类模板 类模板是用来蓝图的,编译器不能为类推断参数类型。必须在尖括号中提供额外的信息,用来代替参数的模板实参列表。
定义类模板 我们实现StrBlib的模板版本,为Bolb,不在针对string,使用时用户需要指出元素类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename T> class Blob {public : t ypedef T value_type; typedef typename std: :vector<T> : :size_type size_type; Blob (); Blob (std: : initializer_list<T> il); 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 : std: : shared ptr<std : : vector<T>> data; void check (size_type i, const std: :string & msg) const ; };
我们的Blob模板有一个名为T的模板类型参数,用来表示Blob保存的元素的类型。例如,我们将元素访问操作的返回类型定义为T&。当用户实例化Blob时,T就会被替换为特定的模板实参类型。
实例化类模板 使用类模板时,提供额外信息,这些信息实际上是显示模板实参列表,它们被绑定到模板参数。
1 2 Blob<int > ia; Blob<int > ia2 = {0 ,1 ,2 ,3 ,4 };
于是编译器生成一个类似这样的类:
1 2 3 4 5 6 7 8 9 template <> class Blob <int > { typedef typename std: :vector<int >: :size_type size_type; Blob (); Blob (std: :initializer_list<int > il); int & operator [](size_type i); private : std: :shared ptr<std: :vector<int >> data; void check (size_type i, const std : : string & msg) const ; };
一个类模板的每个实例都形成一个独立的类。类型Blob与任何其他Blob类型都没有关联,也不会对任何其他B1ob类型的成员有特殊访问权限。
在模板作用域中引用模板类型 一个类模板中的代码如果使用了另外一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。相反的,我们通常将模板自己的参数当作被使用模板的实参。例如,我们的data 成员使用了两个模板,vector和 shared_ptr。我们知道,无论何时使用模板都必须提供模板实参。在本例中,我们提供的模板实参就是Blob的模板参数。因此,data的定义如下:
std: :shared_ ptr<std: : vector<T>> data;
类模板的成员函数 因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
当我们定义一个成员函数时,模板实参与模板形参相同。对于strBlob的一个给定的成员函数
ret-type StrBlob : : member-name(parm-list)
对应的Blob的成员应该是这样的:
template <typename T> ret-type Blob<T>: :member-name(parm-list)
check和元素访问成员 我们首先定义check成员,它检查一个给定的索引:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template <typename T>void Blob<T>: :check (size_type i,const std::string &msg) const { if (i >=data->size ()) throw std: :out_of_range (msg) ; } 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 i) { check (i, "subscript out of range" ) ; return (*data)[i]; } template <typename T> void Blob<T>: :pop_back () { check (0 ,"pop_back on empty Blob" ); data->pop_back (); }
Blob构造函数 与类模板外函数一样,构造函数先定义模板参数:
1 2 template <typename T>Blob<T>::Blob () : data (std: :make_shared<std: :vector<T>>()){ }
分配一个空vcector,并将指向vector的指针保存在data中,还要有接受一个initializer_list参数的构造函数将其类型参数工作为initializer list参数的元素类型:
1 2 3 template <typename T>Blob<T>::Blob (std: :initializer_list<T> il): data (std: :make_shared<std: :vector<T>>(il)){ }
为了使用这个构造函数,我们必须传递给它一个initializer_list,其中的元素必须与Blob的元素类型兼容:
Blob<string> articles = { "a", "an", "the" };
类模板成员的实例化 只有当程序用到它时才会进行实例化
1 2 3 4 5 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;
实例化了 Blob类和它的三个成员函数: operator[ ] 、 size和接受initializer_list的构造函数。
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类内简化模板类名 当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public : BlobPtr () : curr (0 ) {} BlobPtr (Blob<T>& a, size_t sz - 0 ): wptr (a.data), curr (sz) {} T& operator *() const { auto p = check (curr, "dereference past end" ); return (*p)[curr]; } BlobPtr& operator ++(); BlobPtr& operator --( ); private : std: :shared_ptr<std: :vector<T>> check (std : : size_t , const std : : string&) const ; std : : weak_ptr<std : : vector<T>> wptr; std : : size_t curr; };
在递增和递减函数中,我们返回的是BlobPtr&,而不使用BlobPtr&,因为当处于一个类模板的作用域时,自身引用时就等价于
1 2 BlobPtr<T>& operator ++(); BlobPtr<T>& operator --();
在类模板外使用类模板名 由于在类外,只有遇到类名才代表进入类的作用域,所以类外函数返回自身需要使用BlobPtr
1 2 3 4 5 6 7 8 template <typename T>BlobPtr<T> BlobPtr<T>: :operator ++(int ){ BlobPtr ret = *this ; ++*this ; return ret; }
由于函数体已经进入类内,所以可以直接使用BlobPtr。
在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
类模板和友元 当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系 我们的Blob类应该将BlobPtr类和模板版本的Blob相等运算符定义为友元,此外我们在Blob加入可以用==运算符的友元函数:
1 2 3 4 5 6 7 8 9 10 11 12 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类中出现的5个T,说明它将对应的类与函数声明为友元,如:
1 2 Blob<char > ca; Blob<int > ia;
通过和特定的模板友好关系 一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ; };
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
令模板自己的类型参数成为友元 1 2 3 4 template <typename Type> class Bar { friend Type; };
此处我们将用来实例化Bar的类型声明为友元。因此,对于某个类型名Foo,Foo将成为Bar的友元,sales data将成为Bar的友元,依此类推。
模板的类型别名 可以给已经实例化的类起别名:typedef Blob<string> StrBlob;
模板起别名的方式为则不同:
1 2 template <typename T> using twin = pair<T,T>;twin<string> authors;
也可以固定多个模板参数:
1 2 3 4 template <typename T> using partNo = pair<T, unsigned >;partNo<string> books; partNo<Vehicle> cars; partNo<Student> kids;
类模板的static成员 类模板也可以声明static成员:
1 2 3 4 5 6 7 template <typename T> class Foo {public : static std: :size_t count () { return ctr; } private : static std: : size_t ctr; };
这样的static成员会在同一个类型内共享,如Foo这个类型的所有对象,共享这两个函数:
1 2 3 4 Foo<string> fs; Foo<int > fi, fi2,fi3;
数据成员也同样如此,且必须有且仅有一个定义,所有该特定类的对象共享此成员。
1 2 template <typename T>size_t Foo<T> : : ctr = 0 ;
与非模板类的静态成员相同,我们可以通过类类型对象来访问一个类模板的static成员,也可以使用作用域运算符直接访问成员。当然,为了通过类来直接访问static成员,我们必须引用一个特定的实例:
1 2 3 4 Foo<int > fi; auto ct = Foo<int > : :count () ; ct = fi.count (); ct= Foo: :count ();
static成员函数只有在使用时才会实例化。
模板参数 模板参数的名字不仅可以是T还可以是其他任何命名。
模板参数与作用域 一个模板参数可用范围是在其声明之后,至模板声明或定义结束之前。与其他任何名字一样的是模板参数隐藏外层作用域声明的相同的名字,不同的是,在模板内不能重用模板参数名:
1 2 3 4 5 typedef double A;template <typename A,typename B> void f (A a,B b) A tmp = a; double B; }
由于模板名字不能重用,所以在模板参数列表也只能出现一次:
1 2 template <typename v, typename v>
模板声明 模板的声明必须包括模板参数:
1 2 3 template <typename T> int compare (const T&,const T&) ;template <typename T> class Blob ;
个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员 由于使用::运算符在模板参数上就会有困难,如:T::men,它不知道men声明类型成员和static数据成员,所以必须知道这个T是否表示一个类型。
它需要知道我们是正在定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘。
如果希望使用一个类型成员就必须显示使用typename关键字:
1 typename T: :size_type * p;
当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用 class。
默认的模板实参 就像函数的默认实参一样,我们也可以为模板参数提供实参:
1 2 3 4 5 6 7 template <typename T, typename F = less<T>>int compare (const T &v1,const T &v2,Ff = F ()){ if (f (vl, v2) ) return -l; if (f (v2, v1) ) return 1 ; return 0 ; }
用户调用时,可以自己提供,也可以使用默认的:
1 2 3 4 bool i = compare (0 ,42 );sales_data item1 (cin) , item2 (cin) ;bool j = compare (iteml, item2,compareIsbn);
与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时它才可以有默认实参。
模板默认实参与类模板 同样的,类可以使用默认模板参数
1 2 3 4 5 6 7 8 9 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;
成员模板 普通类的成员模板 若一个普通类中有一个模板函数,便被称为成员模板:
1 2 3 4 5 6 7 8 9 10 11 12 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; };
这是一个类似unique_ptr的使用的默认删除器,根据不同的类型进行销毁操作:
1 2 3 4 5 6 double * p = new double ;DebugDelete d; d (p); int * ip = new int ;DebugDelete ()(ip) ;
我们就可以用这个类型替换unique_ptr中的删除器:
1 2 3 4 5 6 unique_ptr<int ,DebugDelete> p (new int ,DebugDelete()) ;unique _ptr<string,DebugDelete> sp (new string,DebugDelete() ) ;
当unique_ptr析构函数调用时,De—类便会实例化:
1 2 3 void DebugDelete: :operator () (int *p) const { delete p; }void DebugDelete: :operator ()(string *p)const { delete p; }
类模板的成员模板 类和成员有各自的模板,即可以像这样定义:
1 2 3 4 template <typename T>class Blob { template <typename It>Blob (It b, It e); };
也可以
1 2 3 4 template <typename T>template <typename It> Blob<T>::Blob (It b, It e): data (std::make_shared<std::vector<T>>(b,e)) { }
实例化与成员模板 为了实例化上一个模板类的成员模板,我们必须同时提供类和函数的实参:
1 2 3 4 5 6 7 8 9 int ia[] ={ 0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 };vector<long > vi = { 0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; list<const char *> w = { "now" , "is" , "the" , "time" }; Blob<int > al (begin(ia), end (ia)) ;Blob<int > a2 (vi.begin (), vi.end ()) ;Blob<string> a3 ( w.begin (), w.end ()) ;
定义a1时就实例化了如下版本:
1 Blob<int > : :Blob (int * , int * );
控制实例化 当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在新标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销。一个显式实例化有如下:
1 2 3 4 5 6 extern template declaration; template declaration; extern template class Blob < string>; template int compare (const int &, const int &) ;
编译器遇到extern声明时,它不会在本文件中生成实例化代码,而是承诺其他地方有这样的实例化,我现在只是使用它,对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
extern声明必须在任何使用此实例之前:
1 2 3 4 5 6 7 8 9 extern template class Blob < string>;extern template int compare (const int &,const int &) ;Blob<string> sal,sa2; Blob<int > al = {0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 } ; Blob<int > a2 (al) ; int i = compare (a1 [0 ],a2[0 ]);
上面的这些使用extern的实例必须在其他地方有定义:
1 2 3 4 template int compare (const int &, const int &) ;template class Blob < string>;
对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
实例化定义会实例化所有成员 在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数。
效率与灵活性 unique_ptr避免了间接调用删除其的运行时开销,而shared_ptr使用户可以重载删除器。前者有效率,后者有灵活度。
模板实参推断 类型转换与模板类型参数 与往常一样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括如下两项。
const转换:可以将一个非 const对象的引用(或指针)传递给一个const的引用(或指针)形参。
数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。
1 2 3 4 5 6 7 8 9 10 11 12 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);
在最后一对调用中,我们传递了数组实参,两个数组大小不同,因此是不同类型。在fobj调用中,数组大小不同无关紧要。两个数组都被转换为指针。fobj中的模板类型为int*。但是,fref调用是不合法的。如果形参是一个引用,则数组不会转换为指针(参见6.2.4节,第195页)。a和 b的类型是不匹配的,因此调用是错误的。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换及数组或函数到指针的转换。
使用相同模板参数类型的函数形参 模板的参数只允许几种有限的类型转换,因此传递的这些形参必须具有相同的类型。我们的compare函数接受两个const T&参数,其实参必须是相同类型:
1 2 long lng;compare (lng,1024 );
如果希望可以类型转换,可以将函数模板定义为两个类型参数
1 2 3 4 5 6 7 template <typename A,typename B>int flexibleCompare (const A& v1,const B& v2) { if (vl< v2) return -1 ; if (v2< v1) return 1 ; return 0 ; }
正常类型转换应用于普通函数实参 1 2 3 4 5 6 7 template <typename T> ostream &print (ostream &os,const T &obj) { return os <<obj; } print (cout,42 ); ofstream f ( "output" ) ;print (f,10 );
第一个函数参数是一个已知类型ostream&。第二个参数 obj 则是模板参数类型。由于os 的类型是固定的,因此当调用print时,传递给它的实参会进行正常的类型转换:
如果函教参教类型不是模板参数,则对实参进行正常的类型转换。
函数模板显式实参 某些时候模板参数无法推断除类型,允许用户控制模板实例化。
指定显式模板实参 我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
1 2 3 template <typename Tl,typename T2, typename T3>Tl sum (T2,T3) ;
没有任何参数可供推断出T1的类型,所以调用时必须提供一个显示模板实参。
1 2 auto val3 = sum<long long >(i, lng);
显式模板实参时一一对应的,只有右边的可以忽略,但必须可以从函数参数推断出来。
1 2 3 template <typename T1, typename T2, typename T3>T3 alternative_sum (T2,T1) ;
则我们总是必须为所有三个形参指定实参:
1 2 3 auto val3 = alternative_sum<long long > (i, lng) ;auto val2 = alternative_sum<long long ,int ,long >(i, lng);
所以把需要显示提供的参数放在最前面
正常类型转换应用于显式指定的实参 如果模板类型参数已经显式指定,页可以进行类型转换
1 2 3 4 long lng;compare (lng,1024 ); compare<long > (lng,1024 ); compare<int > (lng,1024 );
第一个调用由于类型不匹配错误,后面的调用由于显式指定,而可以进行正常的类型转换。
尾置返回类型与类型转换 1 2 3 4 5 template <typename It>??? &fcn (It beg, It end){ return *beg; }
由于我们不知道返回结果的准确类型,但所需类型是所处理的序列的元素类型
1 2 3 4 vector<int > vi = { 1 ,2 ,3 ,4 ,5 }; Blob<string> ca = {"hi" , "bye" }; auto &i = fcn (vi.begin (), vi.end () ); auto &s = fcn (ca.begin (), ca.end ());
我们知道函数应该返回*beg,而且知道我们可以用decltype (*beg)来获取表达式类型。但是,在编译器遇到函数的参数列表之前,beg都是不存在的。为了定义此函数,我们必须使用尾置返回类型。由于尾置返回出现在参数列表之后,它可以使用函数的参数:
1 2 3 4 5 6 template <typename It>auto fcn (It beg,It end) -> decitype (*beg) { return *beg; }
进行类型转换的标准库模板类 如果并不想返回引用而是返回其中的值,可以使用标准库类型转换 模板。在头文件type_traits中,如果我们用一个引用类型实例化remove_reference,则type将表示被引用的类型。例如,如果我们实例化 remove_reference<int&>,则type 成员将是int。类似的,如果我们实例化remove_reference<string&>,则type成员将是string,依此类推。更一般的:
1 remove_reference<decltype (*beg) >::type
组合使用它们就可以在函数中返回元素值的拷贝:
1 2 3 4 5 6 7 8 template <typename It>auto fcn2 (It beg,It end) -> typename remove_reference<decltype (*beg) >::type { return *beg; }
函数指针和实参推断 可以用一个函数模板对一个函数指针进行赋值,可根据形参生成一个实例,被指针所指:
1 2 3 template <typename T> int compare (const T&,const T&) ;int ( *pf1)(const int &, const int &) = compare;
如果不能从函数指针类型却低估模板实参,则产生错误:
1 2 3 4 void func (int (*)(const string&,const string&)) ;void func (int (*) (const int &,const int &)) ;func (compare);
由于既可以接受int和string版本的compare,所以调用失败。不过可以显式的指出:
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
模板实参推断和引用 左值引用函数参数推断类型 一个函数参数是模板类型参数的普通引用时,只能传递给它一个左值:
1 2 3 4 5 template <typename T> void f1 (T&) ;f1 (i);f1 (ci); f1 (5 );
如果是const T&,则推断结果不会是一个const类型
1 2 3 4 5 6 template <typename T> void f2 (const T&) ;f2 (i);f2 (ci); f2 (5 );
从右值引用函数参数推断类型 如果函数参数是右值引用,如T&&,推断出T的类型是该右值实参的类型:
1 2 template <typename T> void f3 (T& &) ;f3 (42 );
引用折叠和右值引用参数 如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。
编写接受右值引用参数的模板函数 1 2 3 4 5 template <typename T> void f3 (T&& val) { Tt = val; t = fcn (t) ; if (val == t){ } }
上面的函数,如果传入42,则T会推断为int,但如果传入int的左值,则T会推断为int&,则如果修改t的同时也会修改val。
1 2 template <typename T> void f (T&& ) ; template <typename T> void f (const T&) ;
通常是这样重载模板函数,与非模板函数一样,第一个版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。
理解std::move 在13.6.2节中我们注意到,虽然不能直接将一个右值引用绑定到一个左值上,但可以用move获得一个绑定到左值上的右值引用。
std::move如何定义 标准库的的move
1 2 3 4 5 template <typename T>typename remove_reference<T>::type&& move (T&& t) { return static_cast <typename remove_reference<T>::type&&> (t); }
这段代码很短,但其中有些微妙之处。首先,move的函数参数T& &是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给move一个左值,也可以传递给它一个右值:
1 2 3 string s1 ( "hi! " ) , s2 ;s2 = std: :move (string ( "bye ! " ) ); s2 = std: :move (sl);
std::move如何工作 如我们已经见到过的,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。因此,在std: : move (string ( "bye ! "))
中:
推断出的T的类型为string。
因此,remove_reference用string进行实例化。
remove_reference的type成员是string。
move的返回类型是string&& 。
move的函数参数t的类型为string& &。
因此,这个调用实例化move,即函数string&& move(string &t)
左值static_cast到右值引用时允许的 我们可以用static_cast显式地将一个左值转换为一个右值引用。
转发 某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
1 2 3 4 5 6 7 template <typename F, typename T1,typename T2>void flip1 (F f,T1 t1,T2 t2) { f (t2,t1); }
如果调用一个接受引用的参数就会出问题:
1 2 3 4 void f (int v1, int &v2) { cout << v1 <<" " <<++v2 <<endl; }
函数第二个参数为引用,说明我们希望通过函数改变原变量的值,但是使用模板调用就会丢失这个引用的属性:
1 2 f (42 ,i); flip1 (f,j,42 );
问题在于j被传递给flip1的参数t1。此参数是一个普通的、非引用的类型int,而非int&。因此,这个flip1调用会实例化为
1 void flip1 (void (*fcn) (int ,int & ), int t1,int t2) ;
定义能保持类型信息的函数参数 通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值〉使得我们可以保持const属性,因为在引用类型中的const是底层的。如果我们将函数参数定义为T1&&和 T2&&,通过引用折叠(参见 16.2.5节,第608页)就可以保持翻转实参的左值/右值属性(参见16.2.5节,第608页):
1 2 3 4 template <typename F, typename T1, typename T2>void flip2 (F f,T1 &&t1,T2 &&t2) { f (t2, t1) ; }
这样调用就传递给t1一个左值j,t1会折叠为int&,则t1会绑定到j上,就可以通过函数改变j的值。
如果一个函数参数是指向模板类型参数的右值引用(如 T&&),它对应的实参的const属性和左值/右值属性将得到保持。
但是不能用于接受右值引用参数的函数:
1 2 3 void g (int & &i, int & j) { cout << i <<" " <<j << endl; }
如果我们试图通过flip2调用g,则参数t2将被传递给g的右值引用参数。即使我们传递一个右值给flip2:
std::forward保持类型信息 1 2 3 4 template <typename Type> intermediary (Type &&arg) { finalFcn (std::forward<Type>(arg)); }
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节。
于是我们可以重写前面的函数:
1 2 3 4 template <typename F, typename Tl,typename T2>void flip (F f,T1 & &t1,T2 &&t2) { f (std: :forward<T2>(t2), std::forward<T1>(t1)) ; }
重载与模板 编写重载模板 首先编写俩个不同的函数模板
1 2 3 4 5 6 template <typename T> string debug_rep (const T &t) { ostringstream ret; ret << t; return ret.str (); }
再定义一个对象对应string表示:
1 2 3 4 5 6 7 8 9 10 11 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 (); }
函数内容并不重要,我们先看使用它们:
1 2 string s ( "hi" ) ;cout << debug_rep (s) << endl;
这个调用只有第一个版本是可行的,第二个版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,因此实参推断失败。
如果用一个指针调用debug_rep
1 cout << debug_rep (&s) <<endl;
两个函数都生成可行的实例:
debug rep(const string*&),由第一个版本的debug_rep实例化而来,T被绑定到string*。
debug rep(string*),由第二个版本的 debug_rep实例化而来,T被绑定到string。
但第二个版本更加精确,第一个版本需要进行普通指针到const的转换,编译器也会选择第二个版本。
多个可行模板 1 2 const string *sp = &s;cout << debug_rep (sp) << endl;
此例中的两个模板都是可行的,而且两个都是精确匹配:
debug rep(const string*&)
,由第一个版本的 debug_rep 实例化而来,T被绑定到string*
。
debug rep(const string*)
,由第二个版本的 debug_rep 实例化而来,T被绑定到const string。
再这种时候编译器会选择最特例化的,我的理解是,最简洁的,则选择第一个版本。
非模板和模板重载 1 2 3 4 string debug_rep (const string &s) { return ' "" +s + ' "'; }
当同时匹配同样好的模板和非模板函数的时候,编译器一定会选择非模板版本。
重载模板和类型转换 如果使用这个调用:
1 2 3 4 5 cout << debug_rep ("hi world!" ) << endl; debug rep (const T&) , debug rep (T*) , debug rep (const strina&) ,
前两个版本都是匹配的,而第二个版本会被认为是精确匹配的,非模板版本是可行的,但需要一次用户定义的类型转换。所以选择第二个。
如果更希望使用字符版本,可以定义:
1 2 3 4 5 6 7 string debug_rep (char *p) { return debug_rep (string (p)); } string debug_rep (const char *P) { return debug_rep (string (p)); }
缺少声明可能导致程序行为异常 为了使用char*
版本的函数,必须提前准备好其中的模板函数声明:
1 2 3 4 5 6 7 template <typename T> string debug_rep (const T &t) ;template <typename T> string debug_rep (T *P) ;string debug_rep (char *p) i
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本。
可变参数模板 一个可变参数模板 就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包 。存在两种参数包:模板参数包 template parameter packet),表示零个或多个模板参数;函数参数包 (function parameteroacket),表示零个或多个函数参数。
1 2 3 4 5 template <typename T, typename ... Args>void foo (const T &t, const Args& ... rest) ;
与往常一样,编译器从函数的实参推断模板参数类型。对于可变参数模板,编译器会推断保重的参数数目:
1 2 3 4 5 6 7 8 9 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…运算符 若需要知道包中的运算符,使用sizeof…运算符:
1 2 3 4 template <typename ... Args> void g (Args ... args) { cout << sizeof ...(Args) <<endl; cout<< sizeof ...(args) << endl; }
编写可变参数函数模板 1 2 3 4 5 6 7 8 9 10 11 12 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); //包中有两个参数
来调用上面的函数,首先会匹配到第二个函数,然后递归调用第二个函数,直至最后一个参数由第一个函数打印。
当定义可变参数版本的 print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。
包扩展 除了获取其大小以外,我们还可以扩展 ,我们还要提供扩展元素的模式 。就时分解为构成的元素,在模式右边放一个省略号(…)触发扩展。
1 2 3 4 5 6 template <typename T, typename ...Args>ostream & print (ostream &os,const T &t,const Args&... rest) { os << t << "," ; return print (os, rest...); }
第一个扩展操作扩展模板参数包,为 print生成函数参数列表。第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
理解包扩展 1 2 3 4 5 6 template <typename ... Args>ostream &errorMsg (ostream &os, const Args&... rest) { return print (os,debug_rep (rest)... ) ; }
在看这样的调用:
1 2 print (os,debug_rep (rest.. .) );
它们的区别就是第一个对扩展包中的每一个调用函数,第二个是在调用中展开:
转发参数包 1 2 3 4 5 class strVec {public : template <class... Args> void emplace_back (Args&&...) ; };
1 2 3 4 5 6 7 template <class... Args>inline void StrVec::emplace_back (Args&&... args) { chk_n_alloc (); alloc.construct (first_free++, std::forward<Args>(args)...); }
emplace_back的函数体调用了chk_n_alloc(参见13.5节,第465页)来确保有足够的空间容纳一个新元素,然后调用了construct在first_free 指向的位置中创建了一个元素。construct调用中的扩展为
std: : forward<Args>(args) ...
它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素
std::forward<T>(t)
其中T,表示模板参数包中第i个元素的类型,t表示函数参数包中第i个元素。例如.假定svec是一个strVec,如果我们调用
svec.emplace_back (10,'c');//将cccccccccc添加为新的尾元素
construct调用中的模式会扩展出
std::forward<int> (10), std::forward<char>(c)
通过在此调用中使用forward,我们保证如果用一个右值调用emplace back,则construct也会得到一个右值。例如,在下面的调用中:
svec.emplace back (s1 + s2);//使用移动构造函数
传递给emplace_back的实参是一个右值,它将以如下形式传递给construct
std: :forward<string> (string ( "the end" ))
forward的结果类型是string&&,因此construct将得到一个右值引用实参。construct会继续将此实参传递给string 的移动构造函数来创建新元素。
建议:转发和可变参数模板
可变参数函数通常将它们的参数转发给其他函数。这种函数通常具有与我们的emp lace_ back函数一样的形式:
1 2 3 4 5 6 7 template <typename ... Args>void fun (Args&&... args) { work (std: : forward<Args>(args)...); }
这里我们希望将fun的所有实参转发给另一个名为work的函数,假定由它完成函数的实际工作。类似emplace_back中对 construct的调用,work调用中的扩展既扩展了模板参数包也扩展了函数参数包。 由于 fun的参数是右值引用,因此我们可以传递给它任意类型的实参;由于我们使用std:: forward传递这些实参,因此它们的所有类型信息在调用work时都会得到保持。
模板特例化 1 2 3 4 5 template <typename T> int compare (const T&, const T& ) ;template <size_t N, size_t M>int compare (const char (&)[N], const char (&)[M]) ;
我们定义了另一个版本的compare,当传递给compare一个字符串字面常量或者一个数组时,编译器才会调用,而传递给它字符指针,就会调用第一个
1 2 3 const char *p1 = "hi" , *p2 = "mom" ;compare (p1, p2);compare ( "hi" ,"mom" );
因为无法将指针转换为数组的引用,因此参数是p1和p2时,第二个版本compare不可行。
为了处理字符指针(而不是数组),可以为第一个版本的compare定义一个模板特例化( template specialization)版本。一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型。
定义函数模板特例化 特例化一个函数模板时,必须为每个模板参数提供实参,在template后跟<>,指出正在实例化:
1 2 3 4 5 template <>int compare (const char * const &p1,const char * const &p2) { return strcmp (p1, p2); }
定义一个特例化版本时,函数的参数类型必须与先前模板中对应类型匹配:
1 template <typename T> int compare (const T&,const T&) ;
特例化中T对应的为const char*,模板函数中为一个常量指针,而我们需要一个指向常量的指针,我们需要在特例化版本中使用的类型是const char * const &,即一个指向const char的const指针的引用。
函数重载与模板特例化 特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。
关键概念:普通作用域规则应用于特例化
为了特例化一个模板,原模板的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。
对于普通类和函数,丢失声明的情况(通常)很容易发现——编译器将不能继续处理我们的代码。但是,如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码。由于在丢失特例化版本时编译器通常会实例化原模板,很容易产生模板及其特例化版本声明顺序导致的错误,而这种错误又很难查找。
如果一个程序使用一个特例化版本,而同时原模板的一个实例具有相同的模板实参集合,就会产生错误。但是,这种错误编译器又无法发现。
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化 我们将has模板定义一个特例化版本,用它保存Sale_data对象,为了让我们自己的数据类型能使用hash,必须定义hash模板的一个特例化版本。一个特例化hash类必须定义:
一个重载的调用运算符(参见14.8节,第506页),它接受一个容器关键字类型的对象,返回一个size_t。
两个类型成员,result type和 argument_type,分别调用运算符的返回类型和参数类型。
默认构造函数和拷贝赋值运算符(可以隐式定义,参见13.1.2节,第443页)。
首先打开命名空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace std{ template <> struct hash < Sales_data> { typedef size_t result_type; typedef sales_data argument_type; size_t operator ( ) (const sales_data& s) const ; }; size_t hash<Sales_data>::operator () (const Sales_data& s) const { return hash<string>() (s.bookNo) ^ hash<unsigned >() (s.units_sold) ^ hash<double >() (s.revenue); } }