1. 引用折叠规则
- 如果间接的创建一个引用的引用,则这些引用就会“折叠”(空格前为模板T类型)。
X& &、X& &&、X&& &都折叠成X&X&& &&折叠为X&&
- 当将一个左值传递给一个参数是右值引用指向模板类型参数
(T&&)时,编译器推断模板参数类型为实参的左值引用
template<typename T>
void f(T&&);
int i = 42;
f(i) //f函数被推导成:void f<int&>(int&)
- 虽然不能隐式的将一个左值转换为右值引用,但是可以通过
static_cast显示地将一个左值转换为一个右值
2. 模板推导
模板的类型推导规则还是蛮复杂的,这里简单描述一下,例如:
template <typename T>
void f(ParamType param);
f(expr);
上面这个例子是函数模板的通用例子,其中T是根据f函数的参数推导出来的,而ParamType则是根据 T 推导出来的。T与ParamType有可能相等,也可能不等,因为ParamType是可以加修饰的。根据ParamType类型可以分为如下几类:
ParamType是值类型:因为是值传递,所以expr的所有修饰特性都会被忽略,const, 引用,volatile等,都被忽略。
template<typename T>
void f(T param); // param 为值传递
f(expr);
int x = 2;
const int cx = x;
const int& rx = x;
int *pp = &x;
f(x); // T : int, param : int
f(cx); // T : int, param : int
f(rx); // T : int , param : int
f(pp); // T是int*,指针比较特殊,直接使用
ParamType是指针或者引用但不是万能引用:ParamType是指针或引用时,引用特性在推导过程中是被忽略的
template <typename T>
void func(T& param);
int x = 10; // x是int
int & rr = x; // rr是 int &
const int cx = x; // cx是const int
const int& rx = x; // rx是const int &
int *pp = &x;
func(x); // T为int
func(cx); // T为const int
func(rx); // T为const int
func(rr); // T为int
func(pp); // T是int*,指针比较特殊,直接使用
//当ParamType是指针或引用时,引用特性在推导过程中是被忽略的。
template<typename T>
void f(const T& param);
int x = 1;
const int cx = x;
const int &rx = x;
f(x); // T的类型是int, param类型是const int&
f(cx); // T的类型是int,param类型是const int&
f(rx); // T的类型是int,param类型是const int&
//当ParamType是指针或引用时,引用特性在推导过程中是被忽略的。
// 同理,由于param已经具有const特性,所以在推导过程中const属性也会被忽略。
template<typename T>
void f(T* param);
int x = 1;
const int *px = &x;
f(&x); // T的类型int, param类型int*
f(px); // T的类型是const int , param类型const int*
ParamType是通用引用类型
template<typename T>
void f(T&& param);
f(expr);
// 如果expr是个左值,则T和paramtype都会推导为左值引用
// 如果expr是个右值,正常推导
int x = 2;
const int cx = x;
const int& rx = x;
f(x); // x 是左值, T的类型为int&, param 为 int&
f(cx); cx : lvalue, T : const int&, param: const int&
f(rx);rx : lvalue, T: const int&, param: const int&
f(2);2: rvalue, T: int, param : int &&
param的类型是最完整的类型,继承了形参中声明的cr(const 和 reference)和实参总带过来的cr。但有两个特例:
- 当形参时通用引用(
T&&作为模板参数时称为通用引用)时,param根据具体的实参类型,推导为左值引用或者右值引用; - 当形参不是引用时,实参到形参为值传递,去除所有
cr修饰符。
T中是否包含cr修饰符,取决于param的修饰符是否已在形参中声明过。即T中修饰符不会与形参中已声明的修饰符重复。
3. typename remove_reference<T>::type&&类型
在 C++11 以前,类成员有成员函数、成员变量、静态成员三种类型,但从 C++11 之后又增加了一种成员称为类型成员。类型成员与静态成员一样,它们都属于类而不属于对象,访问它时也与访问静态成员一样用::访问。
template <typename T>
struct remove_reference{
typedef T type; //定义T的类型别名为type
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;
}
通过上面的代码我们可以知道,经过 remove_reference 处理后, T 的引用被剔除了。假设前面我们通过 move 的类型自动推导得到 T 为 int&&,那么再次经过模板推导 remove_reference 的 type 成员,这样就可以得出 type 的类型为 int 。
4. std::move和std::forward解析
4.1. std::move解析
标准库中move的定义如下:
template<typename T>
typename remove_reference<T>::type && move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
move 函数的参数 T&&是一个指向模板类型参数的右值引用规则,通过引用折叠,此参数可以和任何类型的实参匹配,因此 move 既可以传递一个左值,也可以传递一个右值。
4.1.1. 例子
std::move(string("hello")) 调用解析:
- 首先,根据模板推断规则,确地
T的类型为string; typenameremove_reference<T>::type的结果为string;move函数的参数类型为string&&;static_cast<string&&>(t),t已经是string&&,于是类型转换什么都不做,返回string&&;
strings1("hello"); std::move(s1);调用解析:
- 首先,根据模板推断规则,确定
T的类型为string&; typenameremove_reference<T>::type的结果为stringmove函数的参数类型为string&&&,引用折叠之后为string&;static_cast<string&&>(t),t是string&,经过static_cast之后转换为string&&,返回string&&;
从move的定义可以看出,move自身除了做一些参数的推断之外,返回右值引用本质上还是靠static_cast<T&&>完成的。因此下面两个调用是等价的,std::move就是个语法糖。
void func(int&& a)
{
cout << a << endl;
}
int a = 6;
func(std::move(a));
int b = 10;
func(static_cast<int&&>(b));
4.2. std::forward解析
template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
本质与move一样,都是靠static_cast<>强制转换来完成
5. 通用引用
既然可以接收左值,右可以接收右值。例如move的输入参数。通用类型有两种方式:
typename remove_reference<T>::type &&模板方法auto &&,auto实际是模板中的T
#include <iostream>
template<typename T>
void f(T&& param){
std::cout << "the value is "<< param << std::endl;
}
int main(int argc, char *argv[]){
int a = 123;
auto && b = 5; //通用引用,可以接收右值
int && c = a; //错误,右值引用,不能接收左值
auto && d = a; //通用引用,可以接收左值
const auto && e = a; //错误,加了const就不再是通用引用了
func(a); //通用引用,可以接收左值
func(10); //通用引用,可以接收右值
}
6. 参考文献
C++高阶知识:深入分析移动构造函数及其原理
高速上手C++11/C++14
深入浅出 C++ 11 右值引用
聊聊C++中的完美转发