16.2.1 类型转换与模板类型参数
函数模板只有两种情况的类型转换
const
转换:可以将一个非const
对象的引用(或指针)传递给一个const
的引用(或指针)形参- 数组或函数指针转换:如果函数实参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换
template<typename T> T fobj(T, T);
template<typename T> T fref(const T&, const T&);
string s1("a value");
const string s2("another value");
fobj(s1, s2); // 调用fobj(string, string)
fref(s1, s2); // 调用fref(const string&, const string&)
fobj
是按值传递,需要对实参进行拷贝,所以实参是不是const
都可以fref
是按引用传递,将非const
的s1
转换为const
是允许的
int a[10], b[42];
fobj(a, b); // 调用fobj(int*, int*)
fref(a, b); // 错误,数组类型不同
两个数组大小不同,所以是不同类型
调用fobj
时,数组被转换为指针,所以大小不同没关系
调用fref
时,因为形参是引用,所以数组不会被转换为指针,大小又不一样,所以调用失败
16.2.2 函数模板显示实参
指定显式模板实参
定义一个允许用户控制返回类型的函数模板
template<typename T1, typename T2, typename T3>
T1 sum(T2, T3);
这样每次调用sum
都必须为T1
提供一个显式模板实参
long lng = 1024;
int i = 24;
auto val3 = sum<long long>(i, lng);
还要注意模板参数的顺序,如果返回值是T3
,就必须为T1
,T2
,T3
都指定实参
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换
template<typename T>
int compare(const T &v1, const T &v2);
long lng;
compare(lng, 1024); // 错误,模板参数不匹配 compare(long, int)
compare<long>(lng, 1024); // 正确,compare(long, long)
compare<int>(lng, 1024); // 正确,compare(int, int)
16.2.3 尾置返回类型与类型转换
在模板参数中让用户指定返回类型会给用户增加编程负担,可以使用尾置类型来获取返回类型
比如,我们需要传入容器的迭代器,返回迭代器指向的元素的引用
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
return *beg;
}
fcn
的返回类型与解引用beg
参数的结果类型相同,解引用运算符返回一个左值,因此通过decltype
推断的类型为beg
表示的元素的类型的引用
如果要返回迭代器指向元素的拷贝,可以用标准库的remove_reference
template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
return *beg;
}
注意:type
是一个类的类型成员,所以必须使用typename
来告知编译器,type
表示一个类型
16.2.4 函数指针和实参推断
可以用函数模板给函数指针赋值
template<typename T> int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare;
这里函数指针pf1
的参数类型是int
,所以用函数模板给它赋值时,编译器会使用pf1
的参数类型来推断函数模板实参的类型,所以pf1
指向compare
的int
版本实例
如果不能从函数指针类型推断出模板实参,则报错
// func重载了两个版本
void func(int(*)(const int&, const int&));
void func(int(*)(const string&, const string&));
func(compare); // 错误,不知道调用哪个版本
这里func
无法根据形参来推断compare
的类型参数,所以只能显式地指定类型参数
func(compare<int>);
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
- 模板参数是普通左值引用时,只能传递左值,可以是
const
类型,也可以不是,如果是const
,则T
被推断为const
类型
template<typename T> void f1(T&);
f1(i); // i是一个int,所以T推断为int
f1(ci); // ci是一个const int,所以T推断为const int
f1(5); // 错误,必须传递左值
- 模板参数是
const T&
时,可以传递任何实参
template<typename T> void f2(const T&);
f2(i); // i是一个int,所以T推断为int
f2(ci); // ci是一个const int,所以T推断为int,const已经是类型的一部分了
f2(5); // 一个const &参数可以绑定到右值,所以T推断为int
从右值引用函数参数推断类型
- 当模板参数是右值引用,可以传递右值,也可以是左值
template<typename T> void f3(T&&);
f3(42); // 实参是一个int类型的右值,T推断为int
引用折叠和右值引用参数
正常不能把一个右值引用绑定到一个左值,但有两个例外
- 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时(如
T&&
),编译器推断模板类型参数为实参的左值引用类型。因此,当调用f3(i)
时,编译器推断T
的类型为int&
- 如果我们间接创建一个引用的引用,则这些引用形成了折叠
X& &
、X& &&
和X&& &
都折叠成类型X&
- 类型
X&& &&
折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
这样,给f3
传递左值时,编译器推断T
为左值引用,并使用引用折叠
f3(i); // 实参是一个左值,T推断为int&
f3(ci); // 实参是一个左值,T推断为const int&
f3
的实例化类似于
void f3<int&>(int& &&); // T是int&
折叠成
void f3<int&>(int&);
这两个规则导致了两个重要结果
- 如果一个函数参数是一个指向模板类型参数的右值引用(
T&&
),则它可以被绑定到一个左值 - 如果实参是一个左值,则推断出的模板类型将是一个左值引用,且函数参数将被实例化为一个普通左值引用参数(
T&
)
如果一个函数参数值指向模板参数类型的右值引用,则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用
16.2.6 理解std::move
template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi"), s2;
s2 = std::move(string("bye")); // 实参是右值
s2 = std::move(s1); // 实参是左值
1、传递右值
- 推断T的类型为
string
remove_reference
用string
进行实例化remove_reference<string>
的type
成员是string
move
的返回类型是string&&
move
的函数参数t的类型是string&&
实例化为
string&& move(string&& t) { // 右值
return static_cast<string&&>(t);
}
2、传递左值
T
的类型为string&
remove_reference
用string&
进行实例化remove_reference<string&>
的type
成员是string
move
的返回类型仍是string&&
move
的函数参数t
的类型为string& &&
,会折叠为string&
实例化为
string&& move(string& t) { // 左值
return static_cast<string&&>(t); // 转换为右值
}
这里用static_cast
显式地将一个左值转换为右值引用
16.2.7 转发
template<typename F, typename T1, typename T2>
void flip1(F func, T1 t1, T2 t2) {
func(t2, t1);
}
void f(int v1, int& v2) {
cout << v1 << " " << ++v2 << endl;
}
如果直接调用f
函数
int i = 10;
f(42, i); // 调用结束后i=11,因为是按引用传递
但是在flip1
里面调用就不一样了
int j = 10;
flip1(f, j, 42); // 调用结束后j=10,没有改变
因为j
传递给t1
是按值传递,所以对t1
的改动不会影响到j
如果想对j
改动,就要按引用传递,所以flip1
函数必须能同时支持左值实参和右值实参,要实现这样的功能就需要用右值模板参数
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(t2, t1);
}
int j = 10;
flip2(f, j, 42); // 调用结束后j=11
这样,传递左值j
时,T
推断为int&
,经过引用折叠,t1
的类型为int&
,这样对t1
的修改就是对j
的修改
传递右值42
时,T2
推断为int
,t2
的类型为int&&
如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的
const
属性和左值/右值属性将得到保持
假设现在不调用f
,改成调用g
,把第一个参数改成右值引用
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(t2, t1);
}
void g(int&& v1, int& v2) { // 第一个参数是右值引用
cout << v1 << " " << v2 << endl;
}
flip2(g, j, 42); // 错误,不能将左值实参的t2绑定到右值形参v1
这里传递给t2
是一个右值42
,所以t2
的类型为int&&
,但是t2
是变量,是左值,传递给g
时,因为v1
是右值引用,不能绑定到一个左值,所以报错
在调用中使用std::forward保持类型信息
// 转发左值
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t)
{
return static_cast<T&&>(t);
}
// 转发右值
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t)
{
return static_cast<T&&>(t);
}
我们希望保持42
这个右值的类型信息,不要让它变成左值,可以使用std::forward
,forward
必须通过显式模板参数来调用,返回该实参类型的右值引用,即,forward<T>
的返回类型是T&&
template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
func(std::forward<T2>(t2), std::forward<T1>(t1));
}
flip2(g, j, 42); // 正确
这样,T2
推断为int
,forward
返回类型是int&&
,可以传递给函数g
中的右值引用参数v1
;而T1
推断为int&
,forward
的返回类型为int& &&
,折叠后是int&
,可以传递给函数g
中的左值引用参数v2