本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
C++ Primer 笔记 第十六章
模板(template)属于生成函数&类的一种方式,主要通过替换类型来简化类型不同但其他都相同的主体的定义。
模板的定义由以下两部分组成:
template
一个模板函数的实现:
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
模板参数列表中的 argument 被称为 tempalte argument;这些参数会与函数的参数绑定,而作为类型的 T
将在绑定时由编译器推断出具体的类型。
函数模板再被调用的时候,会根据绑定参数的类型来推断出模板类型的具体类型;这个过程被称为模板的实例化(instantiation)。通过实例化,编译器将为不同类型的参数生成不同的“实例”来供这些参数使用。
以之前的 compare()
为例,由模板定义的参数列表中,T
被称为类型为模板类型的 parameter(template type parameter)。这个参数由关键字 typename
修饰,意味着其可以被当做一种类型来使用,包括但不限于:
// ok: same type used for the return type and parameter
template <typename T> T foo(T* p)
{
T tmp = *p; // tmp will have the type to which p points
// ...
return tmp;
}
表示类型的 模板类型 parameter 需要被关键字 typename
或者 class
修饰,每一个类型都必须拥有独立的关键字:
template <typename T, class U> calc (const T&, const U&);
两个关键字是等价的,但由于 typename
更容易辨识,因此首选该关键字。
除了表示类型的模板 parameter 以外,还有一种表示值的模板 parameter。这种模板参数被成为非类型 parameter(Non-type parameter)。这种参数前会由具体的类型代替 typename
。
当模板实例化的时候,非类型的 parameter 会被替换为函数获取的对应值(或是由编译器推断决定)。由于模板的实例化实在编译期进行,因此该值必须是常量表达式。比如下方的例子,我们可以利用非类型的 parameter 对数组的长度进行替换:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
当调用以下命令时,N
和 M
会被编译器自动替换为 3
和 4
:
compare("hi", "mom");
非类型 parameter 可以表示以下类型:
指针和引用绑定的 argument 必须拥有静态生命周期(static life time),也就是说,指向(引用自)静态的本地对象、动态分配的对象的指针(引用)都是不能作为这种参数的。
绑定 nullptr 的指针可以作为非类型参数。
一切都是常量表达式的要求。
inline
或者 constexpr
函数也可以使用模板。但需要注意的是,关键字 inline
和 constexpr
的位置需要在模板参数列表的后面:
// ok: inline specifier follows the template parameter list
template <typename T> inline T min(const T&, const T&);
// error: incorrect placement of the inline specifier
inline template <typename T> T min(const T&, const T&);
本章开头的例子 compare
中,有几点思路是可以借鉴的:
<
。这是因为很多类型默认定义了 <
,而不是其他的关系运算符;使用 <
可以保证更高的兼容性。该运算符甚至可以被替换为 !=
(如果实际类型是迭代器),或者标准库的 less
仿函数。const
类型。这样可以兼容某些不允许拷贝的类型。模板下的代码生成与普通函数不太一样:
关键概念:
实际操作中,由编写者提供带模板实现的头文件,使用者包含该头文件并提供对应的实例化信息。
模板的编译存在三个阶段:
比如本章开头的 compare
:
if (v1 < v2) return -1; // requires < on objects of type T
如果我们传入没有定义 <
操作的对象,那么该错误是不会再模板自身被编译的时候被检查到的。只有当编译器以该对象实例化后,发现该对象并不能进行 <
运算,才会报错。也就是说,错误是由于实例化的代码不能编译引起的。从这点上看,模板自身是无法保证具体参数的合法性的,这部分内容的合法性需要由使用者保证。
类模板(Class Tempalte)用于类的生成。与函数模板的不同,类模板无法推断模板参数的类型,使用时需要由用户添加。
类模板的定义分几个点:
template <typename T>
T
以 Blob
类为例子:
template <typename T> class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// constructors
Blob();
Blob(std::initializer_list<T> il);
// number of elements in the Blob
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// add and remove elements
void push_back(const T &t) {data->push_back(t);}
// move version; see § 13.6.3 (p. 548)
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
// element access
T& back();
T& operator[](size_type i); // defined in § 14.5 (p. 566)
private:
std::shared_ptr<std::vector<T>> data;
// throws msg if data[i] isn't valid
void check(size_type i, const std::string &msg) const;
};
类模板的实例化需要提供额外的信息。这些信息被称为显式模板实参 (explicit template arguments),比如下面表达式中的 int
:
Blob<int> ia2 = {0,1,2,3,4};
每个类模板的实例化创建的都是一个独立的类类型,相互之间没有任何联系。
比如:
//data definition
std::shared_ptr<std::vector<T>> data;
//if instantiated by Blob<string>
//data type
shared_ptr<vector<string>>
//template parameter list the first
template <typename T>
//which class the member belongs the second
//if return type is the class type, it should include the template parameter as well
return-type
Blob<T>::member_function(parameter_list) { .... }
普通成员中,主要替换的是所属的类,以及返回类型。注意模板关键字与模板参数列表都是必不可少的:
/* check */
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);
}
/* back */
template <typename T>
T& Blob<T>::back()
{
check(0, "back on empty Blob");
return data->back();
}
/* subscript */
template <typename T>
T& Blob<T>::operator[](size_type i)
{
// if i is too big, check will throw, preventing access to a nonexistent element
check(i, "subscript out of range");
return (*data)[i];
}
/* pop_back */
template <typename T> void Blob<T>::pop_back()
{
check(0, "pop_back on empty Blob");
data->pop_back();
}
构造函数中类似。如果构造函数接受任何模板类型的 parameter,需要将类模板的 template parameter 对其类型进行替换:
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 argument。比如下例中的 BlobPtr
,其类型本应该由 BlobPtr
与 <T>
组成,但在类中 <T>
这一部分是可以省略的:
// BlobPtr throws an exception on attempts to access a nonexistent element
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]; // (*p) is the vector to which this object points
}
// increment and decrement
BlobPtr& operator++(); // prefix operators
BlobPtr& operator--();
private:
// check returns a shared_ptr to the vector if the check succeeds
std::shared_ptr<std::vector<T>>
check(std::size_t, const std::string&) const;
// store a weak_ptr, which means the underlying vector might be destroyed
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr; // current position within the array
};
编译器默认所有模板类中的模板类型都拥有 template argument。
如果在模板类外部使用模板类类型,那该类型必须是完全体,也就是模板名 + 模板参数列表 + 模板类所属 + 函数定义的形式,一个都不能少。比如下方的例子
// postfix: increment/decrement the object but return the unchanged value
template <typename T> //template name with template parameter list
BlobPtr<T> BlobPtr<T>::operator++(int) //return type and class to which the function belong, must be wrote in full
{
// no check needed here; the call to prefix increment will do the check
BlobPtr ret = *this; // save the current value
++*this; // advance one element; prefix ++ checks the increment
return ret; // return the saved state
}
需要注意的是,该函数定义内部的作用域属于模板类内部,因此 BlobPtr ret
省略了 template argument 是合法的。
类模板与友元的关系可以大致定义为以下几类:
这些关系的对象可以是模板与具体(函数、类)的关系,也可以是模板与模板之间的关系。
需要提供模板的前置声明。
当友元关系为一对一的关系(One-to-One Friendship)时,其友元关系实际上是属于具体实例之间的友元关系。具体的来说,只有实例化类型匹配的模板,才能访问目标类模板。其声明方法如下:
// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T> bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob
{
// each instantiation of Blob grants access to the version of
// BlobPtr and the equality operator instantiated with the same type
// class friend
friend class BlobPtr<T>;
//function friend
friend bool operator==<T> (const Blob<T>&, const Blob<T>&);
// ...
};
重点在于友元模板类与函数模板之后的那个 template argument <T>
:
friend class BlobPtr<T>;
friend bool operator==<T> (/ .... /)
当这个 <T>
存在,且与目标类模板的一致时,这种一对一的关联就建立起来了。通过以上的声明,友元关系就可以在同实例化类型的类与类(函数)之间建立,比如:
// BlobPtr<char> object is friend class to Blob<char> object
// operator==<char> function is friend function to Blob<char> object
Blob<char> ca;
需要提供模板的前置声明。
除了上述的一种一对一关系以外,如果模板类以某个具体类作为类型进行实例化,那么实例化后的类型将可以以友元的方式访问具体类:
template <typename T> class Pal;
class C { // C is an ordinary, nontemplate class
friend class Pal<C>; // Pal instantiated with class C is a friend to C
};
上例中,Pal<C>
类型的对象可以以友元的方式访问 C
。
不需要提供任何形式的前置声明。
该关系描述的是类模板的所有实例,都是某一个具体类的友元的关系:
class C { // C is an ordinary, nontemplate class
// all instances of Pal2 are friends to C;
// no forward declaration required when we befriend all instantiations
template <typename T> friend class Pal2;
};
以上面的例子来讲,如果模板类 Pal2
不以其实例化类型 Pal2<C>
进行友元关系的声明,那么得到的结果是: Pal2
的所有实例化类型都可以通过友元的方式访问 C
。
不需要提供任何形式的前置声明。
该关系的描述的是:当某个具体类声明了与类模板的友元关系,那么该具体类可以以友元的方式访问类模板的所有实例化类型对象:
template <typename T> class C2 { // C2 is itself a class template
{
// Pal3 is a nontemplate class that is a friend of every instance of C2
friend class Pal3; // prior declaration for Pal3 not needed
};
上面的例子中,具体类 Pal3
可以访问所有 C2
的实例。
不需要提供任何形式的前置声明。
该关系描述的是,在类模板中以非一对一形式声明的友元关系,可以使对应的类模板的所有实例以友元的方式访问目标类模板的所有实例。需要注意的是,这种情况下声明的友元类模板,必须拥有独立的 template argument,否则编译时会报 shadow
错误:
template <typename T> class C2 { // C2 is itself a class template
{
// all instances of Pal2 are friends of each instance of C2; no prior declaration needed
template <typename X> friend class Pal2;
}
上例中, 所有 Pal2
的实例化类型对象都可以以友元的方式访问 C2
的所有实例化对象。注意 Pal2
的 template argument 是 X
,与 C2
的 T
是不一样的。
C++11 中提供了以模板参数类型作为友元的功能:如果类模板以某种类型实例化,且类模板中声明了对该类型的友元关系;那么所有以该种类型定义的对象,函数(甚至 built-in type)都可以以友元的方式访问类模板的该种类型实例:
template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
// ...
};
举例来说,如果 Type
对应的实例类型是 Foo
,那么所有类型为 Foo
的对象,都可以以友元的方式访问 Bar<Foo>
类型的实例。下面是个简单的示例:
template<typename T>
class Bar {
friend T;
protected:
int val= 100;
};
class Foo {
public:
void print_bar(Bar<Foo> &bar){ std::cout<<"bar:\t" << bar.val << std::endl; }
};
int main(int argc, char const *argv[])
{
Bar<Foo> bar;
Foo foo;
foo.print_bar(bar);
return 0;
}
早期的 C++ 可以做类模板实例的 alias:
typedef Blob<string> StrBlob;
C++ 11 中可以对类模板直接进行 alias:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors is a pair<string, string>
对于存在多个参数的情况,可以单独指定某个参数作为 alias:
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books is a pair<string, unsigned>
partNo<Vehicle> cars; // cars is a pair<Vehicle, unsigned>
partNo<Student> kids; // kids is a pair<Student, unsigned>
类模板的静态成员以实例类型为单位:所有类型相同的实例共享一个静态成员,但不同类型的实例之间存在不同的静态成员:
// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;
因为静态成员以实例类型为单位,因此定义时需要加上 template argument:
template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr
与之对应的是,访问静态成员时也需要指定实例化的类型:
Foo<int> fi; // instantiates Foo<int> class
// and the static data member ctr
auto ct = Foo<int>::count(); // instantiates Foo<int>::count
ct = fi.count(); // uses Foo<int>::count
ct = Foo::count(); // error: which template instantiation?
模板参数名字没有内在的意义;T
可以替换为任何的名字。
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // tmp has same type as the template parameter A, not double
double B; // error: redeclares template parameter B
}
上例中,tmp 对应的 A
是模板参数,B
已经作是模板参数了,因此不能在用于内部的 double
声明。
// error: illegal reuse of template parameter name V
template <typename V, typename V> // ...
// declares but does not define compare and Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
// all three uses of calc refer to the same function template
template <typename T> T calc(const T&, const T&); // declaration
template <typename U> U calc(const U&, const U&); // declaration
// definition of the template
template <typename Type>
Type calc(const Type& a, const Type& b) { /* . . . */ }
所有模板的声明应该放置于使用这些模板的代码所在文件的头部。
由于编译器在模板实例化之前无法确定类模板的具体类型,在下例这种情况中,编译器无法确定 T::size_type
是否是类型参数:
T::size_type * p;
T::size_type
是类型时,表达式是在定义名字为 p
的指针T::size_type
是成员变量时,表达式是该变量与 p
的乘积
默认情况会将 T::size_type
不视作类型。如果我们希望将其视作类型,需要在前面显式的加上关键字 typename
:
template <typename T>
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
这种情况下不能使用 class 代替 typename。
C++ 11 中允许在函数 / 类模板中使用默认的 template argument:
// compare has a default template argument, less<T>
// and a default function argument, F()
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;
}
上例中:
F
接受的是一个 callable object 参数,默认值使用了标准库函数 less()
f
绑定了 less
,绑定方式为 F f = F()
调用时如果指定了其他的 callable object,则使用该对象作为比较策略:
bool i = compare(0, 42); // uses less; i is -1
// uses compareIsbn, result depends on the isbns in item1 and item2
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
如果希望只使用默认的模板参数,那么三角括号 <>
对是不能省略的:
template <class T = int> class Numbers { // by default T is int
public:
Numbers(T v = 0): val(v) { }
// various operations on numbers
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // empty <> says we want the default type
成员模板(Member Template)指本身为模板,但同时又作为类或者类模板的成员的成员函数。成员模板不能是虚函数。
// function-object class that calls delete on a given pointer
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr): os(s) { }
// as with any function template, the type of T is deduced by the compiler
template <typename T> void operator()(T *p) const
{ os << "deleting unique_ptr" << std::endl; delete p; }
private:
std::ostream &os;
};
此处重载运算符 ()
可以为指定的指针进行内存释放并打印信息。调用方式如下:
/* bulit-in variable */
double* p = new double;
DebugDelete d; // an object that can act like a delete expression
d(p); // calls DebugDelete::operator()(double*), which deletes p
int* ip = new int;
// calls operator()(int*) on a temporary DebugDelete object
DebugDelete()(ip);
/* unique_ptr */
// destroying the the object to which p points
// instantiates DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// destroying the the object to which sp points
// instantiates DebugDelete::operator()<string>(string*)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
智能指针的构造函数使用方法可以 参考 cpp reference。此处的 deleter 以临时无名对象 DebugDelete()
的形式存在。
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
// ...
};
这里的 T
与 It
会被用于两种不同的函数参数。
template <typename T> // type parameter for the class
template <typename It> // type parameter for the constructor
Blob<T>::Blob(It b, It e):
data(std::make_shared<std::vector<T>>(b, e)) { }
上面的例子展示了成员模板的类外定义的方法,可以看出需要同时写出类模板与成员模板的模板参数列表。
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"};
// instantiates the Blob<int> class
// and the Blob<int> constructor that has two int* parameters
Blob<int> a1(begin(ia), end(ia));
// instantiates the Blob<int> constructor that has
// two vector<long>::iterator parameters
Blob<int> a2(vi.begin(), vi.end());
// instantiates the Blob<string> class and the Blob<string>
// constructor that has two (list<const char*>::iterator parameters
Blob<string> a3(w.begin(), w.end());
由于模板本身并不是类型,因此模板的实例化需要两个部分:
又因为模板实例化是在编译期完成的,因此通常情况下,模板的实例化要求声明与定义同时可见;这也是为什么模板通常把声明和定义都放到头文件中。但这样会带来两个问题:
编译器无法得知所有文件对实例化的需求(也就是说,a.cc
中的模板定义不能被 b.cc
使用),因此每个文件都需要有属于自己的实例化。
C++11: Explicit instantiation declaration vs. explicit instantiation definition
C++ 11 中提供了显式实例化(explicit instantiation)来解决上述的两个问题。显式实例化允许我们手动的指定:
C++ 通过分离实例化的声明和定义来达到这样的效果:
extern template declaration; // instantiation declaration
template declaration; // instantiation definition
这两行实际上代替了之前普通实例化的定义部分:
extern
关键字的这一行,是声明。这行声明告诉我们本文件中只有模板实例化的声明,实例化的定义部分请去别的文件寻找。
需要说明的是,如果是单纯的显式实例化,只需要在使用的地方对模板进行定义即可,并不会用到 extern
。比如下面的结构:
// Foo.h
// no implementation
template <typename T> struct Foo { ... };
//Foo.cpp
#include "Foo.h"
// implementation of Foo's methods
// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float
Foo.cpp
中做了对 Foo.h
的实现。当编译器看到最下方的两行定义时,Foo<int>
类型与 Foo<float>
类型会进行实例化,并将内容存放于 Foo.o
中。extern
的主要作用是抑制所在文件的自动实例化。比如更通常的结构是将声明 / 实现 / 使用完全分开:
<img src=“/_media/programming/cpp/cpp_primer/explict_instantation.svg” width=“400”>
</html>
这种情况下就需要抑制实现文件里的实例化,因为我们只希望在使用的地方实例化。因此:
extern
的声明,因为我们不会在实现文件里使用模板的实例,因此不需要实例化下面是一个具体的三文件结构例子:
/* template.h */
#ifndef TEMPLATE_H
#define TEMPLATE_H
//template declaration
template <typename T> class Test;
#endif
/* template.cc */
#include <iostream>
#include "template.h"
template <typename T> class Test
{
public:
void set(T val) { val = x; }
void print() { std::cout << x; }
private:
T x{};
};
//extern declaration, no instantiation in template.o
extern template class Test<int>;
/* main.cc */
#include "template.h"
#include "template.cc"
//template definition
template class Test<int>;
int main(int argc, char const *argv[])
{
Test<int> it;
it.set(1);
it.print();
return 0;
}
除此之外,函数模板也可以通过这种方式进行显示实例化。以上面的成员函数 set
举例:
/* template.h */
template <typename T> class Test
{
public:
void set(T val); // declaration of set
//....other implementation
};
/* template.cc */
//implementation
template <typename T>
void
Test<T>::set(T val)
{
x = val;
}
//extern declaration
extern template void Test<int>::set(int);
/* main.cc */
//template definition
template void Test<int>::set(int);
//....function call
extern
声明可以存在多个,可以用于任何你不想产生实例的文件.o
文件中。从实现上来说,显式实例化是在链接期完成的在实际的设计过程中,我们可以通过模板类型的特性来帮助我们区分不同的设计需求。比较典型的是智能指针的 deleter:
根据这两种不同的需求,我们会发现:
这两种需求实际上体现了在运行期或者编译期绑定 deleter 的区别。
由于 shared_ptr 要求可以共享并重写 deleter,因此只能通过间接的方式(指针(或者指针类,比如作业中会用到的 std::function
))来存储 deleter。这导致 deleter 的具体定义只能在运行期确认。由于编译期无法确定 deleter 的类型,因此无需为其提供模板参数类型。对应的析构函数的逻辑如下:
// value of del known only at run time; call through a pointer
del ? del(p) : delete p; // del(p) requires run-time jump to del's location
只要指向 deleter 的指针不为空,就调用 deleter 进行析构操作。
deleter 是 unique_ptr 类型的一部分,因此需要在编译期就确认下来。也就是说,我们需要为 unique_ptr 提供另外一个模板参数来指定 deleter 的类型。其析构函数始终使用一开始定义好的 deleter:
// del bound at compile time; direct call to the deleter is instantiated
del(p); // no run-time overhead
模板的参数推断指编译器会根据调用中的 arugment 的类型来确定其模板参数的类型,并用该模板参数来生成满足调用的函数实例。
当函数的 parameter 是模板参数时:
template <typename T> T fobj(T, T); // arguments are copied
template <typename T> T fref(const T&, const T&); // references
string s1("a value");
const string s2("another value");
fobj(s1, s2); // calls fobj(string, string); top const in s2 is ignored
fref(s1, s2); // calls fref(const string&, const string&)
// as a non-const object, s1 will be bind to an reference to const string parameter
int a[10], b[42];
fobj(a, b); // calls f(int*, int*)
// error: array are not converted to pointer here, because fref takes reference type parameter.
// as a result, array are passed as array type. since size of array is a part of its type, thus types don't match here.
fref(a, b);
对于相同的模板参数类型,需要确保最终推断的结果类型完全一致,否则会导致调用错误。比如:
template <typename T>
bool compare(T, T) {....}
long lng;
compare(lng, 1024); // error: cannot instantiate compare(long, int)
由于模板参数的类型转换中不存在算术类型的转换,因此模板推断结果冲突,调用失败。这种情况下可以使用不同的模板参数来解决:
// argument types can differ but must be compatible
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;
}
上述的 compare
最终接受了两种不同的类型;但需要注意针对该两种类型的 <
运算也需要被定义。
函数的 parameter 可以混用模板参数与普通参数。此时普通参数的类型转换遵循普通参数的规则(第六章中的 normal conversion)。
两种情况下需要用户显式的指定模板参数的类型:
带模板的函数(类)使用时,函数名后的三角括号中就是显式指定模板参数类型的地方:
// T1 cannot be deduced: it doesn't appear in the function parameter list
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
// T1 is explicitly specified; T2 and T3 are inferred from the argument types
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
需要注意的是,如果调整了函数中模板参数的位置,那么所有的参数类型必须被指定:
// poor design: users must explicitly specify all three template parameters
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
// error: can't infer initial template parameters
auto val3 = alternative_sum<long long>(i, lng);
// ok: all three parameters are explicitly specified
auto val2 = alternative_sum<long long, int, long>(i, lng);
当模板参数类型被显式指定时,该参数对应的 argument 遵循普通的类型转换。这一点非常实用,可以允许我们突破参数类型推断下的类型冲突:
long lng;
compare(lng, 1024); // error: template parameters don't match
compare<long>(lng, 1024); // ok: instantiates compare(long, long)
compare<int>(lng, 1024); // ok: instantiates compare(int, int)
显式指定模板类型的前提是用户知道应该指定什么类型。但某些情况下,模板类型无法被推断,也无法由用户判断。这种情况下,解决的方式有两种:
通常情形中,造成无法判断类型的主要原因是使用间接方式访问了对象。这种情况下可以以引用的方式返回对象,我们只需要知道这是一种绑定返回对象的引用即可:
template <typename It>
??? &fcn(It beg, It end)
{
// process the range
return *beg; // return a reference to an element from the range
}
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = { "hi", "bye" };
auto &i = fcn(vi.begin(), vi.end()); // fcn should return int&
auto &s = fcn(ca.begin(), ca.end()); // fcn should return string&
C++11 中提供的尾置声明可以用于这种情形,即通过 decltype
判断函数参数的类型。但由于常规方式中,返回值声明先于函数参数列表可见,因此必须要配合尾置返回形式才能使参数列表先可见:
// a trailing return lets us declare the return type after the parameter list is seen
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) //decltype return a reference on lvalue
{
// process the range
return *beg; // return a reference to an element from the range
}
尾置返回的方法还存在一个缺陷。由于 decltype
对左值会返回其引用,因此不适用于我们想直接返回对象类型的情况。为此,标准库中提供了一类模板,允许我们对模板类型进行一系列的修改。该类模板定义于 <type_traits>
头文件中,详情如下:
此处我们使用 remove_reference
就可以将 decltype
返回的引用类型转化为对应的对象类型:
// must use typename to use a type member of a template parameter; see § 16.1.3 (p. 670)
template <typename It>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
// process the range
return *beg; // return a copy of an element from the range
}
这里的 type
是我们得到的对象类型。其本身 remove_reference
模板的成员,通过我们提供的 decltype(*beg)
进行实例化来去除引用。如果我们提供的类型不需要进行转换(比如模板类型本身就是直接类型,而不是指针或者引用),则 type
等同于指定的模板类型。
使用函数模板对函数指针进行初始化或者赋值时,编译器会利用指针的类型来推断模板的 argument:
template <typename T> int compare(const T&, const T&);
// pf1 points to the instantiation int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
pf1
的定义中使用了 int
作为实例化的类型,因此,T
对应的就是 int
。但需要注意的是,当同时存在多个实例化的类型时,编译器是无法进行自动推断的,因为无法决定使用哪个实例化类型:
// overloaded versions of func; each takes a different function pointer type
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare); // error: which instantiation of compare?
上例中,函数 func
同时进行了不同类型的重载,导致编译器无法判断实例化的类型。这种情况下,必须显式的指定实例化的类型:
// ok: explicitly specify which version of compare to instantiate
func(compare<int>); // passing compare(const int&, const int&)
模板的实例化开始时,每个模板参数用于实例化的类型(或者是值)都必须唯一。
当模板参数对应的函数参数是引用的时候,有两种情况需要讨论:
当函数参数类型为 T&
,也就是普通左值引用的时候:
template <typename T> void f1(T&); // argument must be an lvalue
// calls to f1 use the referred-to type of the argument as the template parameter type
f1(i); // i is an int; template parameter T is int
f1(ci); // ci is a const int; template parameter T is const int
f1(5); // error: argument to a & parameter must be an lvalue
当函数参数类型为 const T&
,也就是常量左值引用的时候:
T
的推断类型始终是普通的类型
template <typename T> void f2(const T&); // can take an rvalue
// parameter in f2 is const &; const in the argument is irrelevant
// in each of these three calls, f2's function parameter is inferred as const int&
f2(i); // i is an int; template parameter T is int
f2(ci); // ci is a const int, but template parameter T is int
f2(5); // a const & parameter can be bound to an rvalue; T is int
当函数参数类型为 T&&
时,也就是右值引用的时候:
函数参数类型为右值引用的模板类型,在接受 argument 时,与普通右值引用参数最大的不同,是其可以接收左值。
T
T&
const T&
模板右值引用类型的函数可以接收左值的原因是因为引用折叠(Reference Collapsing )。考虑以下过程:
T&&
模板类型参数接收到 lvaule 时,编译器会将其视作左值引用,也就是推断其类型为 T&
T& &&
T&
上述第三部也就是是我们之前提到的引用折叠。实际上,引用折叠会发生在所有的引用以间接的方式绑定引用的场景中:
T& &
T& &&
T&& &
T&& &&
折叠后的类型推断结果可以总结为:
T&
T&& &&
,则推断结果会折叠为右值引用 T&&
来看一下 f3
的例子:
template <typename T> void f3(T&&);
f3(42); // argument is an rvalue of type int; template parameter T is int
f3(i); // argument is an lvalue; template parameter T is int&
f3(ci); // argument is an lvalue; template parameter T is const int&
当参数类型是左值 i
时,其实际类型为 int& &&
:
// invalid code, for illustration purposes only
void f3<int&>(int& &&); // when T is int&, function parameter is int& &&
但最终被折叠为 int&
:
void f3<int&>(int&); // when T is int&, function parameter collapses to int&
模板类型右值引用参数可以接受左值意味着该类型的参数实际上可以接收任意类型的 argument;也就是说这种情况下,可以被当成右值使用的 argument,也可以被当做左值使用。
以下模板函数中:
template <typename T> void f3(T&& val)
{
T t = val; // copy or binding a reference?
t = fcn(t); // does the assignment change only t or val and t?
if (val == t) { /* ... */ } // always true if T is a reference type
}
val
是右值时,T t = val
是拷贝操作,因此改变 t
不会改变 val
val
是左值时, T t= val
是绑定操作,因次改变 t
也会改变 val
实践中,右值引用参数主要用于两种情况:
std::move
是模板参数引用推断机制的一个非常好的实践例子,其大致的实现如下:
// for the use of typename in the return type and the cast see § 16.1.3 (p. 670)
// remove_reference is covered in § 16.2.3 (p. 684)
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
// static_cast covered in § 4.11.3 (p. 163)
return static_cast<typename remove_reference<T>::type&&>(t);
}
考虑到 std::move
的功能:
可以看到的是,move
的返回值是一定是一个右值引用。因此,我们只需要考虑如何将 t
的类型转换为右值引用返回即可。
那么,上面的设计的思路就很明显了:
std::move
接收任意类型的 argumentstatic_cast
将得到的类型转化为其右值引用并返回考虑下面代码:
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // ok: moving from an rvalue
s2 = std::move(s1); // ok: but after the assigment s1 has indeterminate value
当 std::move
接收右值时:
T
被推断为 stringremove_reference
实例化为 remove_reference<string>
,得到的类型是 stringmove
的返回值类型为 string&&
static_cast<string&&>(t)
;由于 t
是 T&&
,因此不做处理
当 std::move
接受左值 s1
时:
T
被推断为 string&
remove_reference
实例化为 remove_reference<string&>
,通过实例化去除引用,依然得到类型 stringmove
的返回值类型依然是 string&&
static_cast<string&&>(t)
;t
的类型被折叠为 string&
,因此执行 static_cast<string&&>(t)
,结果依然返回 string&&
static_cast 可以转换左值到右值引用又一次体现了 C++ 的设计哲学;提供灵活性,但前提是你要知道你自己在做什么。比如这类情况,转换的前提是要求左值可以被安全的截断;而 static_cast 又是一种显式的强制转换。C++ 通过这种方式将处理权完全移交给了程序员,但同时又通过显式的方式来提醒程序员。
在实际应用中,某些函数需要将接收的 argument 原封不动的转交给其他函数。具体的说,我们希望保存以下的特性:
下面是一个转发过程中 argument 发生变化的例子:
// template that takes a callable and two parameters
// and calls the given callable with the parameters ''flipped''
// flip1 is an incomplete implementation: top-level const and references are lost
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
void f(int v1, int &v2) // note v2 is a reference
{
cout << v1 << " " << ++v2 << endl;
}
f(42, i); // f changes its argument i
flip1(f, j, 42); // f called through flip1 leaves j unchanged
上面是一个函数 flip1
,内嵌一个打印函数 f
,会将接受的参数 t1
,t2
交换顺序进行打印,并对 t2
对应的参数进行自增。但实际上,该程序无法达到预期的效果,因为 t
对应的 argument j
在转发的过程中发生了变化:
t1
以复制的方式接收的是左值 j
,因此 T1
被推断为 intflip1
实际上实例化为:
void flip1(void(*fcn)(int, int&), int t1, int t2);
因此传递给 int &v2
的是独立的拷贝 t2
,而不是 j
。
上面的例子中,存在(潜在)的问题有:
j
to temporary t1
)
根据右值引用以下的特性,将 flip
的函数 parameter 设置为右值引用类型,即可解决大部分问题:
比如下面的 flip2
实现:
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
在之前的调用中:
<img src=“/_media/programming/cpp/cpp_primer/forward_2.svg” width=“500”>
</html>
j
通过引用折叠使 T1
的类型被推断为了 int&
,从而完成了与 t1
的绑定。因此,这个版本的 f
可以成功修改 j
值。
上述的方法中存在一个问题:
f
是一个普通的函数
这带来的问题是,如果普通函数中的参数类型是右值引用,而上游传递的参数类型是左值(引用),那么普通函数是无法完成左值到右值的转换的。比如将 f
改为下面 g
:
void g(int &&i, int& j)
{
cout << i << " " << j << endl;
}
这种情况下,我们无法将一个左值(引用) t2
传递给 i
:
<img src=“/_media/programming/cpp/cpp_primer/forward_3_2.svg” width=“500”>
</html>
标准哭中提供了模板 std::foward
用于处理此类情况。std::forward
的 parameter 类型为 T&&
,通过指定其参数的模板类型 T,从而返回该模板类型的右值引用,比如 forward<T>
会返回 T&&
。该模板定义于 <utility>
头文件中。具体的使用方式:
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}
其实现原理也使用了引用折叠:
T& &&
,最终折叠为 T&
std::forward<T&>
,也就是 T& &&
,最终折叠为 T&
T
std::forward<T>
,也就是 T&&
std::forward
主要:
std::forward 不应使用 using 声明。
当函数模板参与到重载函数的匹配中时:
当同时存在几个最佳匹配结果的时候,还需要额外考虑:
以书上的内容为例,实现一个 debug_rep()
函数,该函数接收一个对象,并将对象的内容以 string 的形式返回。
首先,我们实现两个版本的 debug_rep()
:
const T&
T*
实现如下:
// print any type we don't otherwise handle
template <typename T> string debug_rep(const T &t)
{
ostringstream ret; // see § 8.3 (p. 321)
ret << t; // uses T's output operator to print a representation of t
return ret.str(); // return a copy of the string to which ret is bound
}
// print pointers as their pointer value, followed by the object to which the pointer points
template <typename T> string debug_rep(T *p)
{
ostringstream ret;
ret << "pointer: " << p; // print the pointer's own value
if (p)
ret << " " << debug_rep(*p); // print the value to which p points
else
ret << " null pointer"; // or indicate that the p is null
return ret.str(); // return a copy of the string to which ret is bound
}
如果进行以下调用:
string s("hi");
cout << debug_rep(s) << endl;
s
在此处是 string 类型,因此:
const T&
的版本可以接受 s
,T
被推断为 stringT*
的版本无法实例化,因为编译器无法通过一个非指针类型的 argument 去推断一个指针的类型
因此,此处选则 const T&
版本
上述实现中,如果将调用改变为:
cout << debug_rep(&s) << endl;
那么两个版本的函数模板都会成为 viable:
const T&
版本接受指针,T
被推断为 string*
,最后的实例化结果为 debug_rep(const string* &)
T*
版本接受指针,T
被推断为 string
,最后的实例化结果为 debug_rep(string* &)
由于 const T&
argument 会发生 constness 的改变(rank2),因此 T*
版本是最佳匹配。
将上面的调用改为:
const string *sp = &s;
cout << debug_rep(sp) << endl;
const T&
版本接受 const pointer to string,T
被推断为 const string*
。最终实例化的结果为 debug_rep(const string* &)
T*
版本接受 ,T
被推断为 const string
,最终实例化结果为 debug_rep(const string*)
由于编译器无法分辨指针与指针的引用之间的区别,此时两者均是普通意义上的最佳匹配(exactly match,普通函数到这一步已经会导致二义性了)。但需要注意的是,由于 const T&
可以接受的 argument 类型更加广泛(包括但不限于指针),因此 T*
版本是更加特殊的版本。根据之前的规则,T*
版本是最佳匹配。
我们再为 debug_rep()
添加一个普通函数的重载:
// print strings inside double quotes
string debug_rep(const string &s)
{
return '"' + s + '"';
}
当以下面命令进行调用时:
string s("hi");
cout << debug_rep(s) << endl;
候选有两个:
const T&
, T
被推断为 string,实例化结果为 const string &
const string&
此处提供的参数类型为 const string
类型,因此两个函数在普通意义上都是最佳匹配。根据之前的规则,当候选者中存在唯一的,非模板函数时,该函数版本为最佳匹配版本。
考虑以下调用:
cout << debug_rep("hi world!") << endl; // calls debug_rep(T*)
const T&
版本:T
会被推断为数组类型 char[10]
T*
版本: T
会被推断为 const char
const string&
版本:const char* → string
由于普通非模板版本中存在低等级的类型转换,因此首先排除该版本。到此,const T&
与 T*
版本都是普通意义上的最佳匹配;根据规则,选择更特殊的版本 T*
实际实现中,我们更倾向于为 const char*
设计两个非模板版本的重载,并使用其调用接受 string 的普通版本:
// convert the character pointers to string and call the string version of debug_rep
string debug_rep(char *p)
{
return debug_rep(string(p));
}
string debug_rep(const char *p)
{
return debug_rep(string(p));
}
上面的例子中,我们使用 char*
版本的 debug_rep()
调用了 const T&
版本的 debug_rep()
。如果不对该版本进行前置声明,编译器很可能会自动选择之前的某个其他版本:
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
// the following declaration must be in scope
// for the definition of debug_rep(char*) to do the right thing
string debug_rep(const string &);
string debug_rep(char *p)
{
// if the declaration for the version that takes a const string& is not in scope
// the return will call debug_rep(const T&) with T instantiated to string
return debug_rep(string(p));
}
这是因为对于未声明函数的调用,普通函数与成员函数的处理方式不同:
上面的例子中,如果我们不对普通的 const string&
版本进行前置声明,则编译器会自行调用 const T&
版本。
C++11 提供了 Variadic template,用于支持处理多个不同类型的参数。Variadic template 使用参数包(parameter pack)对参数进行管理。定义 Variadic template 时,参数包需要分为两部分定义:
Variadic template 通过省略号参数(ellipsis)来表示参数包。模板参数包定义的是类型,而该类型会被用于定义函数参数包:
// Args is a template parameter pack; rest is a function parameter pack
// Args represents zero or more template type parameters
// rest represents zero or more function parameters
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
在对 Variadic template 进行类型推断的时候,编译器还会根据 argument 的数量来推断参数包中参数的数量:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // three parameters in the pack
foo(s, 42, "hi"); // two parameters in the pack
foo(d, s); // one parameter in the pack
foo("hi"); // empty pack
/* instantiation */
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…
运算符进行计算。该运算符不会执行 argument:
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // number of type parameters
cout << sizeof...(args) << endl; // number of function parameters
}
sizeof…
实际上是 sizeof
与省略号运算符的组合。这是 pack expansion 的一个最基础应用;后面将会提到。
C++ 可以通过 initializer_list
接受数量不同的参数;但这些参数需要有相同的类型。使用 Variadic template 可以允许我们编写打印数量不同,并且类型也不同的参数。
在设计上,由于 Variadic template 经常与递归函数搭配。这是因为参数包的处理分为两步:
因此,我们的打印函数 print()
也应该以递归的方式来实现。也就是说,print()
需要一个 non-variadic 的版本来结束递归。具体实现如下:
// function to end the recursion and print the last element
// this function must be declared before the variadic version of print is defined
template<typename T>
ostream &print(ostream &os, const T &t)
{
return os << t; // no separator after the last element in the pack
}
// this version of print will be called for all but the last element in the pack
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
os << t << ", "; // print the first argument
return print(os, rest...); // recursive call; print the other arguments
}
上面的实现中,variadic 版本的 print()
需要三个参数:
ostream &print(ostream &os, const T &t, const Args&... rest)
而其内部的递归调用中,print()
只有两个参数:
return print(os, rest...);
之所以可以这么做,当 argument 以参数包的形式进行传递时,可以以参数组合的方式进行接收。也就是说,我们传递到下个层级 ’‘print()’‘ 参数包 rest
,在传递的时候被分为了两个部分:
<html>
<img src=“/_media/programming/cpp/cpp_primer/print_imp_r.svg” width=“600”>
</html>
通这样的递归调用,参数包中的参数将逐步减少。当参数包中只存在一个参数时,候选这将会有两个。比如下面的调用:
print(cout, i, s, 42); // two parameters in the pack
该递归最后一次调用将只有一个参数:
print(cout, 42);
此时,我们的 variadic 版本 和 non-variadic 版本的 print
都是 viable(variadic 版本中,当前递归层级的 rest
为空)。由于 non-variadic 版本更加特殊,因此最后会调用该版本,并使递归成功结束。
习题中有一个问题,如果 non-variadic 版本 对 variadic 版本不可见会导致什么后果?
书上给出了 warning,说会导致无限递归:
A declaration for the nonvariadic version of print must be in scope when the variadic version is defined. Otherwise, the variadic function will recurse indefinitely.
但在实践过程中,编译无法通过,会报没有匹配函数的错误。这一点其实不难解释:从之前的例子可以看出来,当参数包为空的时候并不会 “占用” 参数的位置。如果不存在 non-variadic 的版本,那么该递归将继续进行到 print(cout)
;此时没有任何一个 print()
的版本可以对应这个调用,因此编译器报错。
我们可以通过提供指定的 pattern 对参数包进行扩展:
partten + …
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest)// expand Args
{
os << t << ", ";
return print(os, rest...); // expand rest
}
上例中,我们对 print()
进行了两个部分的拓展:
而通过扩展,print()
可以达到以下效果:
print(cout, i, s, 42); // two parameters in the pack
//instantiation
ostream& print(ostream&, const int&, const string&, const int&);
//equivalent call
print(os, s, 42);
函数也可以作为 pattern 应用到参数包中的每一个元素上:
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an)
return print(os, debug_rep(rest)...);
}
上例 print()
调用中添加了 debug_rep(rest)
这样的 pattern,因此包中每个成员都会作为参数执行一遍 debug_rep
。也就是说,如果调用 errorMsg
:
errorMsg(cerr, a,b,c,d);
实际上是调用了:
print(cerr, debug_rep(a), debug_rep(b), debug_rep(c), debug_rep(d));
注意区分两种写法:
由于 variadic template 往往是基于已有的工具函数进行适配,参数传递在其实现过程中是很常见的。为了保证参数传递的准确性,variadic template 往往与 std::forward
一起使用。
emplace_back() 是基于 variadic template 开发出来的,以 strvec 为例,其实现大致如下:
/* declaration */
class StrVec {
public:
template <class... Args> void emplace_back(Args&&...);
// remaining members as in § 13.5 (p. 526)
};
/* defination */
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args)
{
chk_n_alloc(); // reallocates the StrVec if necessary
alloc.construct(first_free++, std::forward<Args>(args)...);
}
上例中的两个个要点:
consturct
函数中,以 std::forward<Args>(args)
作为 parrten,对调用进行了扩展其中第二个要点:
std::forward<Args>(args)...
实际上指对于每个实例化的类型,以及其对应的参数,都进行转发:
//Ti represents the type of the ith element in the template parameter pack
//ti represents the ith element in the function parameter pack
std::forward<Ti>(ti)
比如:
svec.emplace_back(10, 'c'); // adds cccccccccc as a new last element
实际上参数包被拓展为:
std::forward<int>(10), std::forward<char>(c);
也就是以当前地址为起始,构建 10 个内容为 c
的 char 变量 。
Variadic Templates + Forwarding 是一种很经典的设计方式。这种类型的函数都有两个特点:
std::forward
的拓展其优势在于,可以接受任何数量/类型的参数并将这些参数原封不动转发给工具参数。
单个模板往往不能处理所有的问题。当某些类型需要特殊处理时,我们可以基于当前的模板,为其定义一个额外的模板版本进行处理。定义这个模板的过程被成为模板的特化(template specialization)。
以下面的例子为例:
template <typename T> int compare(const T&, const T&);
通常情况下,用于比较的两个对象都是通过运算符来实现的。但当我们需要比较两个 literal string 时,用于比较的方法是 strcmp
。此时一般版本的 compare
就不适用了,我们需要定义对应的特化版本:
// special version of compare to handle pointers to character arrays
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}
特化版本以 template<>
起头,后面紧跟具体的返回值以及参数列表。返回值与参数列表必须匹配原有的模板,比如本例中:
const char*
类型const T&
类型T
替换为特化版本需要处理的类型const char* const&
需要特别注意的是,特化遵循普通的作用域规则。有几点要注意:
推荐的做法:
类模板也可以针对特定的类型进行特化。
在无序关联容器一章中有过自定义哈希值的计算方法。当时采用了提供哈希函数的方式;另一种方法是对标准库的哈希函数进行特化。
特化标准库的哈希函数有几个准备工作:
()
,该运算符接收容器的 key 作为参数,并返回 size_t
类型的返回值result_type
与 argument_type
除此之外,由于 std::hash
处于命名空间 std
中,因此对其的特化需要在同一个命名空间下才能生效:
// open the std namespace so we can specialize std::hash
namespace std {
} // close the std namespace; note: no semicolon after the close curly
下面是实现方式:
// open the std namespace so we can specialize std::hash
namespace std {
template <> // we're defining a specialization with
struct hash<Sales_data> // the template parameter of Sales_data
{
// the type used to hash an unordered container must define these types
typedef size_t result_type;
typedef Sales_data argument_type; // by default, this type needs ==
size_t operator()(const Sales_data& s) const;
// our class uses synthesized copy control and default constructor
};
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);
}
} // close the std namespace; note: no semicolon after the close curly
几个要点:
Sales_data
的哈希函数由指定的三个成员的哈希函数进行或运算得到std::equal_to<Key>
指定兼容的 Sales_data 版本的 ==
运算符,方便构造hash<Sales_data>
实例需要作为 Sales_data
的友元(也可单独友元运算符 ()
)Sales_data
定义在同一个 header 中,保证用户可以使用
特化后的类 / 函数不再是模板。本例中,因为特化版本定义在了 Sales_data.h
中, 如果该头文件被多个文件引用,会造成特化版本的多重定义。解决方案通常是将其声明为 inline
。另外一种方法是文件外定义,但书中并不建议如此。
类模板的特化允许只为源模板提供一部分 argument;这种方式的特化被成为类模板的偏特化(Class-Template Partial Specializations)。偏特化有两个限制:
Type transformation 模板中的 remove_reference
是一个很好的例子:
// original, most general template
template <class T> struct remove_reference {
typedef T type;
};
// partial specializations that will be used for lvalue and rvalue references
template <class T> struct remove_reference<T&> // lvalue references
{ typedef T type; };
template <class T> struct remove_reference<T&&> // rvalue references
{ typedef T type; };
可以看出来的是:
在调用中,偏特化的类型会优先调用对应版本:
int i;
// decltype(42) is int, uses the original template
remove_reference<decltype(42)>::type a;
// decltype(i) is int&, uses first (T&) partial specialization
remove_reference<decltype(i)>::type b;
// decltype(std::move(i)) is int&&, uses second (i.e., T&&) partial specialization
remove_reference<decltype(std::move(i))>::type c;
C++ 也允许单独对类模板的常规能源进行特化。比如下面的例子,Foo
类模板的 bar()
成员进行了特化:
template <typename T> struct Foo {
Foo(const T &t = T()): mem(t) { }
void Bar() { /* ... */ }
T mem;
// other members of Foo
};
template<> // we're specializing a template
void Foo<int>::Bar() // we're specializing the Bar member of Foo<int>
{
// do whatever specialized processing that applies to int
}
该 Bar
版本属于 Foo<int>
的成员。当使用 Foo<int>
类型的对象对 Bar
进行调用时,会优先调用该版本:
Foo<string> fs; // instantiates Foo<string>::Foo()
fs.Bar(); // instantiates Foo<string>::Bar()
Foo<int> fi; // instantiates Foo<int>::Foo()
fi.Bar(); // uses our specialization of Foo<int>::Bar()