面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。不同之处:OOP能处理类型在程序运行之前都未知的情况;而泛型编程中,在编译时就能获知类型了
模板参数类别不能为空。
模板参数表示在类或函数定义中用到的类型或值。
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;//T为int
编译器生成的版本通常被称为模板的实例。
特别是,类型参数可以用来指导返回类型或函数参数类型,以及在函数内用于变量声明或类型转换:
类型参数前必须加上class或typename(typename是在模板已经广泛使用之后才引入c++语言的)
非类型行参数必须是常量表达式:template<unsigned N, unsigned M>
当一个模板实例化时,非类型参数被一个用户提供的或编译器推导出来的值所代替。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型参数的实参时一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
函数模板可以声明成inline或constexptr
template<typename T> inline T min(const T&, const T&);
compare函数说明了编写泛型代码的两个重要原则:
模板中的函数参数是const的引用。
函数体中的条件判断仅使用<比较运算
通过将函数参数这位const引用,我们保证函数可以用于不能拷贝的类型。而且,处理大对象,这种设计策略还能使函数运行得更快。
只使用小于,降低了对处理对象的类型要求,不用支持>
是实际上,如果真的关系类型无关和可移植性,可能需要用less来定义,(不用less,存在的问题是,如果用户调用它比较两个指针,二期两个指针为指向相同的数组,则代码的行为未定义。
template<typename T> int compare(const T &v1, const T &v2)
{
if(less<T>()(v1,v2)) return -1;
if(less<T>()(v2,v1)) return 1;
return 0;
}
模板程序应该尽量减少对实参类型的要求。
当编译器遇到一个模版定义是,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板是,编译器才生成代码。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用类对象时,类定义必须是可用的,但是成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而将普通函数和类的成员函数的定义放在源文件中。
但是模板不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成原函数的定义。因此,模板的头文件通常即包含声明也包含定义。
当使用模板是,所有不依赖模板参数的名字都必须是可见的,这是有模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义包括类模板的成员定义都是可见的。
用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。。
模板知道实例化是才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。通常编译器在三个阶段报告错误。
第一阶段是编译模板本身时。在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误。
第二阶段是编译器遇到模板使用时。在此阶段,编译器仍然没有很多课检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。它还检查参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参。
第三个阶段是模板实例化时,只有在这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接是才报告。
编译器不能为类模板推断模板参数类型。必须使用尖括号中提供的额外信息。
template <typename T>class Blob(
public:
typedef T value_type;
typedef typename std::vector<T>::size_tyep size_type;
Blob();
Blob(std::initializer_list<T> il);
size_type size( ) cosnt {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;
} ;
一个类模板的每个实例都是一个相互独立的类。
应该记住类模板的名字不是一个类型名,而是用来实例化类型,而一个实例化的类型总是包含模板参数的。
一个模板中的代码若果使用了另一个模板,我们通常将模板自己的参数作为被使用模板的实参。
std::shared_ptr<std::vector<T>> data;
类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。
template<typename T>
void Blob<T>::check(size_type i, const std::string &msg)const
{
if (i >= data->size( ))
throw std::out_of_range(mag);
}
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>
Blob<T>::Blob( ) : data(std::make_shared<std::vector<T>>( ) ) { }
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>> (il)) { }
在默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参,当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
template <typename T>class BlobPtr{
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 ,cosnt std::string&) const;
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
};
当我们在类模板外定义其成员是,必须记住,我们并不在类的作用域中,知道遇到类名才表示进入类的作用域:
template<typename T>
BlobPtr<T>& BlobPtr<T>::operator++(int)
{
BlobPtr ret = *this ;
++ *this;
return ret;
}
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
类模板与另一个模板友好关系最常见的形式是建立对应实例及其友元间的友好关系。
为了引用模板的一个特定实例,我们必须首先声明模板自身。
template <typename> class BlobPtr;
template <typename> class Blob;//==中的参数所需的。
template <typename T>
bool operator == (const Blob<T>& ,const Blob<T>&) ;
template<typename T> class Blob{
//每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator == <T> (const Blob<T>& , const Blob<T>&);
};
如果想让所有实例都成为友元,友元声明必须使用与类模板本身不同的模板参数。
//前置声明,在将模板的一个特定实例声明为友元是用到。
template<typename T> class Pal;
class C{//C非模板
friend class Pal<C>;//用C实例化的Pal是C的一个友元
tempalte <typename T> friend class Pal2; //Pal2的所有实例都是C的友元;这种情况无须前置声明
};
template <typename> class C2{
//C2的每个实例将相同实例化的Pal声明为友元
friend class Pal<T>; //Pal的模板啥呢么必须在作用域内
//Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
//Pal3是一个非模板类,它是C2所有实例的友元
friend class Pal3; //不需要Pal3 的前置声明。
};
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的参数。
在新标准中,我们可以将模板参数类型声明为友元:
template <typename Type> class Bar{
friend Type; // 将访问权限授予用来实例化Bar的类型。
}
值得注意的是,通常友元应该是一个类或一个函数,此时我们也允许用内置类型,一边我们能用内置类型来实例化这样的类。
我们可以用模板实例定义typedef,但是不可以用模板。
但是在新标准,我们可以用用using类定义一个模板的别名;
template <typename T> using twin = pair<T , T>;
template<typename T>using partNo = pair<T, unsigned>;
这个代码中我们将partNo定义为一族类型的别名,这族类型是second成员为unsigned的pair
对于static成员来说,每个模板的实例中的static都是相互独立的,定义是也必须使用模板,访问也是
Template <typename T>
size_t Foo<T>::ctr = 0;//定义并初始化。
auto ct = Foo<int> :: count();
类似任何其他成员函数,一个static成员函数只有在被使用时才会实例化。
模板参数和普通的参数一样。
模板声明必须包含参数
与函数参数相同,声明中的模板参数的名字不必与定义中相同。
我们用作用域类访问static成员或类型成员。在普通代码中,编译器掌握类的定义。因此它知道通过作用域访问符访问的名字是类型还是static成员。但是对于模板代码就存在困难:
T::size_type *p;
他需要知道我们是定义一个名为p的变量还是将一个名为size_type的static数据成员与名为p的变量相乘;
默认情况,c++语言假定通过作用域访问符的名字不是类型。因此,如果我们希望使用一个模板参数的数据成员,就必须显示的告诉编译器该名字是一个类型。我们通过是用typename实现:
template <typename T>
typename T::value_type top(const T& c)
{
if(!c.empty() )
return c.back()
else
return typename T::value_type( );
};
当我希望通知编译器以个名字表示类型是,必须使用typename 不能用class
新标准中,我们可以为函数和类模板提供默认实参。而更早的版本只允许为类模板提供默认实参。
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;
}
与函数默认实参一样,对于一个模板参数只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
如果我们为所有的模板参数都提供了默认实参,我们在使用模板的时候,同样应该在模板名后加一对尖括号。
成员模板不能使虚函数。
普通类的成员模板:
class DebugDelete{
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) { }
template <typename T> void operator() (T *p) const
{ os << "deleting unique_ptr"<<std::end; deletep; }
private:
std::ostream &os;
};
unique_ptr<int , DebugDelete>p(new int , DebugDelete( ));
对于类模板,我们也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。
template <typename T> class Blob{
template <typename It> Blob (It b, Ite);
};
当我们在类外定义,必须同时为类模板和成员模板提供各自的参数,注意顺序类模板在前
template<typename T>
template<typename It>
Blob<T>::Blob(It b, It e) : data(std::make_shared<std::vector<T>>(b,e)){ }
为了实例或一个类模板,我们必须同时提供类和函数模板的实参。我们在哪个对象上吊用成员模板,编译器就根据对象的类型类推断类模板参数的实参。编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
Blob<int> a1(begin(ia) , end(ia)); //通过ia的类型类推断成员模板的参数类型。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中都会有该模板的一个实例。这样可能开销很大
在新标准中,我们可以通过显式实例化类避免这种开销。
//实例化声明与定义
extern template class Blob<string>; //声明
template int compare(const int& , const int&) ; //定义
当编译器遇到extern模板声明时,它不会在文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的以个非extern声明(定义),对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
一个类的实例化会定义实例化该模板的所有成员。与普通的实例化不同。因此我们来显示实例化一个类模板的类型,必须能用于模板的所有成员。
对于shared_ptr和unique_ptr,前者给予我们共享指针所有权的能力,而后者则独占。
另一个差异是它们允许用户重载默认删除器的方式不同。对于shared_ptr我们只要在创建或Reset指针时传递给它以个可到用对象即可。与之相反,删除器的类型是一个unique_ptr的一部分。用户必须在定义unique_ptr是以显式模板实参的形式提供删除器的类型。对于后者来说,提供自己的删除去更为复杂。
推断出,在标准库中,shared_ptr必须能直接访问其删除器。即,删除器保存为一个指针或一个封装了指针的类。
我们可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道。实际上,在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类型的删除器构造一个shared_ptr,随后使用reset赋予此shared_ptr另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此不能直接保存删除器。
是在运行时绑定shared_ptr的删除器的。
unique_ptr可能的工作方式。在这个类中删除器是类类型的一部分。模板参数的第二个表示删除器类型。因此删除器是在编译时就知道的,从而删除器可以直接保存在unique_ptr对象中。
通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便。
从函数实参类确定模板实参的过程被称为模板实参推断。
如果以个函数形参的类型使用了模板类型实参,那么它采用特殊的初始化原则。只有很有限的集中类型转换会自动地应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
与往常以样,顶层const无论是在形参中还是在实参中,都会被忽略。在其他转换中,能在调用中应用于函数模板的包括如下两项:
const转换:可以将一个非const对象的引用(或指针)传递给一个const的引用
数组或函数指针的转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其受元素的指针。类似的,一个函数实参可以转换为以个该函数类型的指针。
其他类型的转换都不能应用于函数模板。
如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
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)
显示模板实参按由左至右的顺序匹配,而且只有尾部参数的显示模板实参才可以或略,而且前提是它们可以从函数参数推断出来。
对于普通函数,允许正常的类型转换,同样,当模板类型的参数已经显示指定了函数参数,就相当于已经初始化了模板参数,也进行正常的类想转换。:
long lng;
compare(lng,1024);//模板参数不匹配
compare<long>(lng, 1024);//实例化compare(long, long);
显示指定模板参数会增加用户的负担。除此之外我们可以使用后置返回类型,使用decltype来获取参数类别中表达式的类型。
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg)
{
return *beg;
}
所有的迭代器操作都不会生成元素,只能生成元素的引用。对于我们来说如果我们要返回迭代器中的元素类型,是无法知道的。
为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板在头文件type_traits中。这个头文件中的类通常用于模板元编程。
remove_reference<decltype(*beg)>::type 可以脱去引用。
auto fcn2<It beg, Itend) -> typename remove_reference<decltype(*beg)> :: type
{
return *beg;
}
注意,type是一个类的成员,而该类依赖于一个模板参数,因此,我们必须在返回类型的声明中使用typename 类告诉编译器,type表示一个类型。
对Mod<T>,其中Mod为 |
若T为 |
则Mod<T>::type为 |
remove_reference |
X&或X&&
否则 |
X
T |
add_const |
X&、const X或函数 否则 |
T const T |
add_lvalue_reference |
x& x&& 否则 |
T X& T& |
add_rvalue_reference |
X&或X&&
否则 |
T
T&& |
remove_pointer |
x* 否则 |
X
T |
add_pointer |
X& 或X&&
否则 |
X* T* |
make_signed |
unsigned X 否则 |
X
T |
make_unsigned |
带符号类型
否则 |
unsigned X T |
remove_extent |
X[n] 否则 |
X T |
remove_all_extents |
X[n1][n2]... 否则 |
X T |
当我们用一个函数模板初始化一个函数指针或为函数指针赋值时,编辑器使用指针的类型类推断模板实参。
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
在函数类型推断是,非常重要的两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。
当一个函数参数是模板类型参数的一个普通(左值)引用时,绑定规则告诉我们,只能传递给它一个左值。如果实参时const的,则T将被推断为const类型;
如果一个参数是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参:一个对象(const或非const),一个临时对象或是一个字面常量值。当函数参数本身是const时,T的类型推断的结果不会是一个const类型。const已经是函数参数类型的一部分;因此,它不会也是模板参数的一部分。
从右值引用推断和左值类似。
我们通常不能将一个右值引用绑定到一个左值上。但是有两个例外。这是move能正确工作的基础。
第一个列外规则影响右值引用参数的推断如何进行。当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。
通常我们不能(直接)定义一个引用的引用。但是,通过类型别名或通过模板类型参数间接定义是可以的。
在这种情况下,我们可以使用第二个例外绑定规则:如果我们间接创建一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了第一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,对于一个给定类型X:
X& &、X& &&和X&& & 都折叠成类型X &
类型X&& &&折叠成X&&
引用折叠只能引用于间接创建的引用的引用,如类型别名或模板参数。
这两个规则导致了两个重要结果:
如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值上;且
如果实参时一个左值,则推断出阿狸的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)
如果一个函数参数是指向模板参数类型的右值引用,则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。
当代码中涉及的类型可能是普通(非引用)类型,也可能使引用类型时,编写正确的代码就变得异常困难(虽然remove_reference这样的类型转换类可能会有帮助)
在实际中,右值引用通常用于两种情况:模板转发其参数或模板被重载。
template <typename T> void f(T&&); //绑定到非const右值
template<typename T> void f(const T&); //左值和const右值
与非模板函数一样,第一版本将绑定到可修改的右值,而第二个版本将绑定到左值或const右值。
虽然我们不能直接将一个右值引用banging到一个左值上,但可以用move获得一个绑定到左值上的右值引用。它本质上接受任何类型的实参,因此我们不会惊讶于它是一个函数模板。
标准库是这样定义move的:
template<typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
通常,static_cast只能作用于其他合法的类型转换。但是,这里又有一条针对引用的特许规则:虽然不能隐式地将一个左值转换为右值,但是可以用static_cast显示地将一个左值转换为右值引用。
对于右值引用绑定到一个左值的特许允许它们阶段左值。我们知道截断一个左值是安全的。一方面,通过允许进行这样的转换,c++语言认可了这种用法。但令一方面,通过强制使用static_cast,c++语言试图阻止我们以为的进行这种转换。
虽然,我们可以直接编写这样的转换代码,但是标准库move函数是容易的多的方式。而且,统一使用std::move是的我们在程序中查找潜在的截断左值的代码变得容易。
通过将一个函数参数定义为一个指向模板类型的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值还是右值)是的我们可以保持const属性,以为在引用类型中的const是底层的。如果我们将函数参数定义为T1& 和T2&&,通过引用折叠就可以保持翻转实参的左值/右值属性。
template<typename F, typename T1, typename T2>
void flip( F f, T1 &&t1, T2 &&t2)
{
f(t2,t1);
}
void f(int v1 , int &v2)
{
cout<<v1<<" "<<++v2<<endl;
}
但是上述版本不能用于接收右值引用参数的函数。
void g(int &&i, int& j)
{
cout<<i << " " << j << endl;
}
flip(g, i, 42)// 错误不能从一个左值实例化int &&
当用于一个指向模板参数类型的右值引用函数参数时,forward会保持实参类型的所有细节。(在utility中)
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&T2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
同move一样,不使用using声明。
函数模板可以被另一个模板或普通非模板函数重载。
如果涉及函数模板,则函数匹配规则会在以下几个方面受到影响:
对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
与往常一样,可行函数(模板与非模板)按类型转换来排序。当然,可以用于函数模板调用的类型转换非常有限。
与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是如果有多个函数提供同样好的匹配,则
如果同样好的函数中只有一个是非模板函数,则选择此函数。
如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
否则,此调用有歧义。
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编辑器由于为遇到你希望调用的函数而实例化一个并非你所需的版本。
可变数目的参数被称为参数包。两种参数包:模板参数包:表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
我们用省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class... 或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
template<typename T, typename... Args>
void foo(const T &t, const Args& ...rest);
和往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。
当我们需要知道包中有多少元素是,可以使用sizeof...运算符。
template<typename... Args> void g(Args... args)
{
cout<<sizeof...(Args) <<endl;
cout<<sizeof...(args) <<endl;
}
我们可以使用initializer_list来定义以个可接受可变数目实参的函数。但是,所有实参必须具有相同类型。
可变参数的函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
template<typename T, typename...Args>
ostream &print(ostream &os, const T &t, const Args&...rest)
{
os<<t<<", ";
return print(os, rest...);
}
当两个函数提供同样好的匹配。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本。
当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无限递归。
对于一个参数包,除了获取其大小外,我们能对他做的危机的事情就是扩展。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成元素,对于每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号类出发扩展操作。
template <typename T,typename... Args>
ostream & print(ostream &os, const T &t, const Args&... rest) ///扩展Args
{
os<< t << ", ";
return print(os, rest...);
}
第一个扩展操作扩展模板参数包,为print生成函数参数列表。第二个扩展操作出现在对print的调用中。此模式为print调用生成参数类别。
对Args的扩展中, 编译器模式将const Args& 应用到模板参数包Args中每个元素。
第二个扩展发生在对print的(递归)调用中 , 模式是函数包的名字。此模式扩展出一个由包中元素组成的、逗号分隔的列表。
c++语言还允许更复杂的扩展模式。如,我们可以编写第二个可变参数函数, 对其每个参数调用debug_rep,然后调用print打印结果
template<typename...Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
return print(os, debug_rep(rest)...);
}
这个print调用使用了模式debug_reg(rest)。
扩展中的模式会独立地应用于包中的每个元素。
在新标准中,我们可以组合使用可变参数模板与forward机制类编写函数, 实现将其实参不变地传递给其他函数。
类似于标准库的emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造一个元素。
class StrVec{
public:
templace <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)... );
}
可变参数函数通常将他们的参数转发给其他函数。
在某些情况下,通用模板的定义对特定类型是不适合的:通用定义可能编译失败或做得不正确。其他时候,我们也可以利用某些特定只是来编写更高效的代码,而不是从通用模板实例化。当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
template <typename T>int compare(cosnt T&, const T&);
template<size_t N,size_t M>
int compare(const char (&)[N], const char (&) [M]) ;
我们无法将一个指针转换为一个数组的引用,第二个版本的compare是不可行的。
为了处理字符指针(而不是数组), 可以为第一个版本的compare定义一个模板特例化版本。一个特例化版本就是模板的一个独立定义,在其中一个或多个模板参数被指定为特定的类型。
当我们特例化一个函数模板时,必须为原模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用template后跟一对空尖括号。
template<>
//compare 的特殊版本,处理字符数组的指针
int compare(const char* const &p1,const char* const &p2)
{
return strcmp(p1, p2);
}
当我们定义一个特例化版本时, 函数参数类型必须与一个先前声明的模板中对应的类型匹配。
我们的函数要求一个指向此类型const版本的引用。 一个指针类型的const版本是一个常量指针而不是指向const类型的指针。我们需要在特例化版本中使用的类型是const char* const &,即一个指向const char的const指针的引用。
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模板的一个特殊实例提供了定义。重要的是弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
我们将一个特殊的函数定义为一个特例化还是一个独立的非模板函数,会影响到函数匹配。但是我们会有效选择更特化的。
为了特化一个模板,原模板的什么必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。
对于普通的声明,丢失就会报错。但是,特化的模板未声明会从模板中实例化的。
如果一个程序使用一个特例化版本, 而同时原模板的一个实例具有相同的模板实参集合, 就会产生错误。但是,这种错误编译器又无法发现。
template <typename T> string debug_rep(const T &t)
{
ostringstream ret;
ret<<t;
return ret.str( );
}
此函数可以用来生成一个对象对应的string表示,该对象可以是任意具备输出运算符的类型。
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();
}
考虑下面例子:
const string *sp = &s;
cout<< debug_rep(sp) <<endl;
有两个都是精确匹配:
debug_rep(const string*&)
debug_rep(const string*)
正常函数无法区分这两个函数。但是,根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*),即更特化的版本。
这条规则的这几原因是,没有它,将无法对一个const的指针调用指针版本的debug_rep。
string debug_rep ( const string &s)
{
return ‘"‘ + s + ‘"‘;
}
还有一个例子,C风格字符转指针和字符串字面常量:
cout << debgu_rep("hi world!") << endl;
所有三个debug_rep版本都可行的
debug_rep(cosnt T&); T被绑定到char[10]
debug_rep(const T*), T被绑定到const char
debug_rep(const string &) , 要求从const char*到string的类型转换;
对给定实参来说,两个模板都提供精确匹配——第二个模板需要进行一次数组到指针的转换,而对于函数匹配来书,这种准话被认为是精确匹配。非模板版本是可行的,但需要一次用户定义的类型转换,因此它没有精确匹配好,所有两个模板成为可能调用的函数。与之前一样T*版本更特例化,编译器会选它。
为标准库的hash模板定义一个特例化版本:
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);
}
}
这里重载调用运算符必须为给定类型的值定义一个哈希函数。对于一个给定值,任何时候调用此函数都应该返回相同的结果。一个好的哈希函数对不相等的对象(几乎总是)应该产生不同的结果。
值得注意的是,我们的hash函数计算所有三个数据成员的哈希值,从而与我们为Sales_data定义的operator==是兼容的。
当我们的特例化版本在作用域中,当将Sales_data作为容器的关键字类型时,编译器会自动使用实例化版本:
unoredred_multiset<Saled_data> SDset;
由于hash<Sales_data>使用Sales_data的私有成员,我们必须将它声明为Sales_data的友元:
template<class T> class std::hash;//友元声明所需要的
class Sales_data{
friend class std::hash<Sales_data>;
};
与函数模板不同,类模板的特例化不必为所有模板参数提供参数。我们可以只指定一部分而非所有模板参数,或者是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中为指定的模板参数提供实参。
标准库中的remove_reference类型就是通过一系列的特例化版本类完成其功能的:
//原始的、最通用的版本
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&&>
{ typeder T type; };
我们可以只特例化特定成员而不是特例化整个模板。
template <typename T> struct Foo{
Foo(const T &t = T() ) : mem(t) { }
void Bar( ){ /*...*/ }
T mem;
};
template<>
void Foo<int> :: Bar( )
{
//进行引用于int的特例化处理。
}