C++Primer 第十六章模板与泛型编程
第十六章 模板与泛型编程
16.1 定义模板
16.1.1 函数模板
template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;
}
模板定义以关键字template
开始,后面跟上一个模板参数列表,编译器会根据传递实参的类型来推断模板参数的类型。
模板类型参数
template<typename T> T foo(T *p) {
T tmp = *p;
return tmp;
}
template<typename T, class U> T calc(const T&, const U&);
类型参数可以作为内置类型或类类型说明符来使用,也可以用来指定函数的返回类型。使用类型参数前必须使用关键字class
或typename
。
非类型模板参数
template<unsigned N, unsigned M>
int compare1(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1, p2);
}
std::cout << compare1("hi", "mom") << std::endl;
非类型模板参数的模板实参必须是常量表达式。
函数模板可以声明为inline
或constexpr
的,但是必须放在模板参数列表之后。
template<typename T> inline T min(const T&, const T&); //right
inline template<typename T> T min(const T&, const T&); //error
模板编译
函数模板何类模板成员函数的定义通常放在头文件中。
16.1.2 类模板
类模板与函数模板不同,编译器无法推断类模板模板参数类型。
定义类模板
template<typename T> class Blob {
public:
using value_type = T;
using size_type = std::vector<T>::size_type;
Blob();
Blob(std::initializer_list<T> li);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const T &t) { data->push_back(t); }
//移动版本的push_back
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<int> ia;
Blob<string> names;
Blob<double> prices;
编译器会从Blob模板实例化出一类类,重写Blob模板,将模板参数T的每个实例替换为给定的模板实参。如果我们在类的外部定义成员函数,需要在成员函数之前加上模板参数列表,因为类模板的成员函数具有和模板相同的模板参数。
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>
ret-type Blob<T>::memeber-name(parm-list)
类模板的成员函数只有在使用的时候才会实例化。
Blob<int> squares = {0,1,2,3,4}; //实力换initializer_list构造函数
for (size_t i = 0; i != squares.size(); ++ i)
squares[i] = i * i; //实例化Blob下标运算符
在类模板自己的作用域中,可以直接使用模板名而不提供实参。注意只有遇到类模板名的时候才表示进入类模板作用域。
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, const std::string &) const;
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr;
};
template <typename T>
BlobPtr<T>& BlobPtr<T>::operator++() {
BlobPtr ret = *this; //在类的作用域内,无需重复模板实参
++*this;
return ret; //返回保存的状态
}
一对一友好关系
//Blob友元需要,需要进行前置声明
template<typename T> class BlobPtr;
template<typename T> 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; //BlobPtr<char>和operator==<char>是本对象的友元
BlobPtr<char>
的成员可以访问ca的非public
部分,但是ca
对ia
或Blob的任何其他实例都没有特殊访问权限。
通用和特定的模板友好关系
template <typename T> class Pal;
class C {
friend class Pal<C>; //用类C实例化的Pal是C的一个友元
//Pal2的所有实例都是C的友元,此时无需前置声明
template <typename T> friend class Pal2;
};
template <typename T> class C2 {
//C2的每个实例将相同实例化的Pal声明为友元
friend class Pal<T>;
//Pal2的每个实例都是C2的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
//Pal3是一个非模板类,它是C2所有实例的友元
friend class Pal3; //不需要Pal3的前置声明
};
为了让所有实例成为友元,友元声明必须使用与类模板本身不同的模板参数。
template <typename Type> class Bar {
friend Type; //将访问权限授予用来实例化Bar的类型
};
将用来实例化Bar的类型声明为友元,因此对于某个类型别名Foo
,Foo
将成为Bar<Foo>
的友元,Sales_data
成为Bar<Sales_data>
的友元。
模板类型别名
模板不是一个类型,无法定义一个typedef
引用Blob<T>
。但是我们可以使用using
来使用类型别名。
template<typename T> using twin = std::pair<T,T>;
template<typename T> using partNo = std::pair<T,unsigned>;
twin<std::string> authors; //authors是一个pair<string,string>
twin<int> win_loss; //win_loss是一个pair<int,int>
twin<double> area; //area,是一个pair<double,double>
partNo<std::string> books; //books是一个pair<string,unsigned>
类模板的static
成员
对任意给定类型,都有一个Foo<X>::ctr
和Foo<X>::count
成员,所有Foo<X>
类型的对象共享相同的ctr对象和count对象。
template<typename T> size_t Foo<T>::ctr = 0; //定义并初始化ctr
//实例化static成员Foo<string>::cstr, Foo<string>::count
Foo<std::string> fs,fs2,fs3;
//所有三个对象共享相同的Foo<int>::ctr和Foo<int>::count成员
Foo<int> fi, fi2, fi3;
auto ct = Foo<int>::count(); //实例化Foo<int>::count
ct = fi.count(); //使用Foo::count
//ct = Foo::count(); //error,不知道使用哪个版本的count
16.1.3 模板参数
在模板内不能重用模板名.
template <typename A, typename B> void f(A a, B b) {
A tmp = a;
double B; //error,重声明模板参数B
}
模板声明和模板定义的参数可以不同。
//三个都是同一个模板
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&, const Type&);
使用类的类型成员
使用一个模板类型参数的类型成员,必须显式告诉编译器该名字是一个类型,使用关键字typename
来实现这一点。
template <typename T>
typename T::value_type(const T& c) {
if (!c.empty()) return c.back();
else
return typename T::value_type();
}
默认模板实参
template <typename T, typename F = std::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 表示可调用对象类型,并定义了一个新的函数参数f,绑定到一个可调用对象上。和函数默认实参一样,对于一个模板参数,只有当它右侧的所有对象都有默认实参时,它才可以有默认实参。
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;
average_precision
用int
代替T实例化得到,lots_of_precision
用long double
代替T实例化得到。
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); //调用DebugDelete::operator释放p
int *ip = new int;
DebugDelete()(ip);
std::unique_ptr<int, DebugDelete> up(new int, DebugDelete());
std::unique_ptr<std::string, DebugDelete> sp(new std::string, DebugDelete());
当unique_ptr
调用析构函数的时候会对成员模板实例化。
//成员模板实例化
void DebugDelete::operator() (int *p) const { delete p; }
类模板的成员模板
类模板和其成员模板的模板参数不同,并且在类的外部定义成员模板,先加上类模板的模板参数,再加上成员模板的模板参数。
template<typename T>
template<typename Iterator>
Blob<T>::Blob(Iterator b, Iterator e) {
data(std::make_shared<std::vector<T>>(b, e));
}
16.1.5 控制实例化
如果多个源文件使用同一个模板,但是模板只有在执行的时候才会实例化,这样就会导致同一个模板被多次实例化,造成大量不必要的开销。通过显式实例化的方式来降低这种开销。
extern template declaration; //实例化声明
template declaration; //实例化定义
再实例化声明的地方模板执行的时候不会实例化,只有在实例化定义的时候模板才会实例化。
//Application.cc
extern template class Blob<string>;
extern template int compare(const int&, const int &);
//templateBuild.cc
template int compare(const int &, const int &);
template class Blob<string>;
再编译的时候一定要将Application.o
和templateBuild.o
连接在一起。
16.2 模板实参推断
从函数实参来确定模板实参的过程叫做模板实参推断。
16.2.1 类型转换与模板参数类型
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const
转换及数组或函数到指针的转换。
可以将一个非const
对象的引用传递给一个const
引用形参。
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);
int main(int argc, char const *argv[])
{
std::string s1("a value");
const std::string s2("another value");
fobj(s1, s2); //const被忽略
fref(s1, s2);
int a[10], b[42];
fobj(a, b);
fref(a, b); //error,数组类型不匹配
return 0;
}
使用相同模板参数的函数形参,传递过来的实参也必须是相同类型。如果想要进行正常的类型转换,可以将模板参数定义为不同的类型.
template <typename T> bool compare(T, T);
long lng;
compare(lng, 1024); //error,类型不匹配
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2);
flexibleCompare(lng, 1024);
16.2.2 函数模板显式实参
指定显式模板实参
如果没有任何函数实参类型来推断模板实参的类型,可以使用显示模板实参类推断。
template <typename T1, typename T2, typename T3>
T1 sum(T2,T3);
int i;
long lng;
auto val3 = sum<long long>(i, lng); //long long sum(int, long)
显式模板实参的类型推断是按照从左至右依次进行,不能搞错顺序。
//糟糕的设计,用户必须指定三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
//无法推断前几个模板参数
auto val2 = alternative_sum<long long>(i, lng);
//right,显式地指定所有三个参数
auto val3 = alternative_sum<long long, int, long>(i, lng);
模板类型参数显式地指定地函数实参可以进行正常的类型转换。
compare<long>(lng, 1024); //right,实例化compare(long, long);
compare<int>(lng, 1024); //right,实例化compare(int, int);
16.2.3 尾置返回类型与类型转换
如果无法确定函数的返回类型,并且显式指定模板参数给用户带来负担,我们可以采用尾置返回类型的办法。
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
return *beg;
}
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
如果函数模板是一个普通类型的引用,只能传递给它一个左值,实参可以是const
也可以不是。
template <typename T> void f1(T&);
int i;
const int ci = 5;
f1(i); //i是int,模板参数类型也为int
f1(ci); //ci是一个const int 模板参数T是const int
f1(5); //error: 传递给一个&参数必须是一个左值
template <typename T> void f2(const T&);
f2(i);
f2(ci);
f2(5);
右值引用的推断和左值引用推断类似。
引用折叠和右值引用参数
template <typename T> void f3(T &&);
f3(42);
如果将一个左值传递给T &&
,如int
类型,编译器会将T推断为int &
类型。如果间接的创建一个引用的引用,会形成引用折叠,引用会折叠成一个普通的引用类型。比如
X& &
、X& &&
和X&& &
都折叠成类型X&
X&& &&
、折叠成X&&
。
因此如果一个函数参数是指向模板参数类型的右值引用,可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,函数参数被实例化为一个普通的左值引用
16.2.6 理解std::move
template <typename T>
typename std::remove_reference<T>::type&& move(T&& t) {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
std::string s1("hi!"), s2;
s2 = std::move(std::string("bye!")); //right,从一个右值移动数据
s2 = std::move(s1); //right,但是赋值之后s1的值无法确定
std::move(std::string("bye!"))
的工作流程:
- 推断出T的类型为
string
remove_reference
用string
进行实例化remove_reference<string>
的type成员为string
- move的返回类型为
string&&
- move的函数参数类型为
string&&
- 所以不需要
static_cast
进行强制类型转化
当move调用s1(一个左值)的时候,执行流程:
- 推断出T的类型为
string &
remove_reference
用string&
进行实例化remove_reference
的type成员为string
- move的返回类型为
string&&
- move的函数参数类型t实例化为
string& &&
经过类型折叠为string &
- 调用的实例化为
string&& move(string &t)
在使用强制类型转换为string&&
16.2.7 转发
某些函数需要将其实参连同类型不变地转发给其他函数,此时我们需要保持被转发实参的所有性质。
void f(int v1, int &v2) {
std::cout << v1 << " " << ++v2 << std::endl;
}
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2) {
f(t2, t1);
}
int j = 0;
f(42, j); //f改变了实参j
flip1(f, j, 42); //通过flip1调用f不会改变j
return 0;
j传递给的是t2是一个普通的int
,不会影响j。
定义能保持类型
如果一个函数参数是指向模板类型参数的右值引用(T &&),它对应的实参的const属性和左值/右值属性将得到保持。
template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
flip2(f, j, 42);
j为左值,传递给T1,T1为int&
,然后t1为int& &&t1
,通过引用折叠t1变为int &t1
,然后在通过引用绑定到v2。
使用forward
来保持类型信息
forward必须通过显式模板实参来调用。forward返回该显示实参类型的右值引用。forward<T>
的返回类型是T&&
.
用一个指向模板参数类型的右值引用函数参数T&&
时,forward会保持实参类型的所有细节。
16.3 重载与模板
template <typename T>
std::string debug_rep(const T &t) {
std::ostringstream ret;
ret << t;
return ret.str();
}
template <typename T>
std::string debug_rep(T *p) {
std::ostringstream ret;
ret << "pointer: " << p;
if (p)
ret << " " << debug_rep(*p);
else
ret << " null pointer";
return ret.str();
}
string s("hi");
cout << debug_rep(s) << endl;
通过精确匹配,此时只有第一个版本可以调用。
cout << debug_rep(&s) << endl;
此时两个函数都可以调用,但是通过精确匹配,debug_rep(T *p)
版本。
如果有多个可行的版本可以调用,选择特例化的版本。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
16.4 可变参数模板
可变数目的参数成为参数包,模板参数包包含零个或多个模板参数,函数参数包包含零个或多个函数参数。在class...
or typename...
为函数参数包的写法。
template <typename T, typename...Args>
void foo(const T &t, const Args&...rest);
int i = 0;
double d = 3.14;
std::string s = "how now brown cow";
foo(i, s, 42, d); //包含三个参数
foo(s, 42, "hi"); //包含两个参数
foo(d, s); //包中有一个参数
foo("hi"); //空包
用sizeof...
可以计算出类型参数的数目。
16.4.2 包扩展
通过在模式右边放一个省略号来触发扩展操作。
template <typename T, typename... Args>
std::ostream & print(std::ostream &os, const T &t, const Args&... rest) { //扩展Args
os << t << " ";
return print(os, rest...); //扩展rest
}
Args的扩展,编译器将模式const Arg&
应用到模板参数包Args中的每个元素,每个类型都是const type&
.
print(cout, i, s, 42); //扩展包中有两个参数
//扩展模式实例化
ostream& print(ostream &, const int&, const string&, const int&);
第二个扩展等价于print(os, s, 42)
.
template <typename... Args>
std::ostream &errorMsg(std::ostream &os, const Args&... rest) {
return print(os, debug_rep(rest)...);
}
debug_rep(rest)...
表示希望对rest中的每个元素都调用debug_rep
。
errorMsg(std::cerr, fcnName, code.num(), otherData, "other", item);
//扩展结果
print(std::cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(ohterData),
debug_rep("otherData"), debug_rep(item));
但是如果是print(os, debug_rep(rest...))
就会编译失败,因为没有函数debug_rep(fcname, code.num(), otherData, "otherData", item)
16.5 模板特例化
特例化的本质是实例化一个模板,而非重载。因此特例化不影响函数匹配。模板及其特例化版本声明放在同一个头文件中。同名模板的声明放在前面,然后是这些模板的特例化版本。