来源:https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
参考:http://bajamircea.github.io/coding/cpp/2016/04/07/move-forward.html
c++11最重要的特性也许就是右值引用了;move语义跟完美转发都是基于右值引用建立起来的。
右值引用的声明形式跟左值引用的声明一样,只是多了一个‘&’。但是并非有‘&&’出现就表明这是一个右值引用。下面有四个例子:
Widget&& var1 = someWidget; // here, “&&” means rvalue reference
auto&& var2 = var1; // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
“&&”出现在类型声明时,可能有下面两种意思:
- 能确定下来,就是右值引用
- 不能确定下来,可以是右值引用或左值引用,要看实际上下文来定。这种情况,这种引用,有个专门的词,叫forwarding reference(转发引用)
区分上面的两种情况还是很有挑战性的,我们先一句话说明一下:
- 如果一个变量或参数的声明有T&&的形式,并且T是需要推导的类型(deduced type T),那么这个变量或参数是个forwarding reference.(划重点,deduced type T)
哪里有type deduction,哪里才有forwarding reference.一般下面几种情况可能会出现forwarding reference:
- parameters to function templates
- auto-declared variables
- typedef
- decltype
forwarding reference必然有T&&的形式。
像所有其他引用一样,forwarding reference必须初始化。初始化决定了,该引用是左值引用还是右值引用:
- 被一个左值初始化,那么就是左值引用
- 被一个右值初始化,那么就是右值引用
如何区分左值还是右值?
如何可以拿到一个expression的地址,那么这个expression是个左值。如果expression是个左值引用 (e.g., T& or const T&, etc.), 那么这个expression是个左值。
其他情况都是右值。右值指的就是临时对象(temporary objects),比如函数返回值,通过隐式类型转换(implicit type conversions)生成的对象。 大部分字面值(literal values )(e.g., 10 and 5.3) 都是右值。
再次考虑下文章开头的代码:
Widget&& var1 = someWidget;
auto&& var2 = var1;
可以获取var1的地址,所有var1是个左值。var2有类型推导,有T&&的形式,是个forwarding reference,因为使用var1(一个左值)来初始化的,所以var2 变成了一个左值引用. var2的声明跟下面的等效:
Widget& var2 = var1;
如果一个expression是左值引用,那么它是一个左值:
std::vector<int> v;
...
auto&& val = v[0]; // std::vector<int>::operator[]返回vector中的一个元素的左值引用,val变成了左值引用
template<typename T>
void f(T&& param); // “&&” might mean rvalue reference,Given this call to f,
f(10); // 10 is an rvalue
参数通过10来初始化,10是个右值。forwarding reference参数通过右值初始化,所以参数变成了一个右值引用,确切说是int&&。
如果 f 像下面这样调用
int x = 10;
f(x); // x is an lvalue
参数通过变量x初始化, x是个左值. forwarding reference参数通过左值初始化,所以参数变成了一个左值引用,确切说是int&。
The comment next to the declaration of f should now be clear: whether param’s type is an lvalue reference or an rvalue reference depends on what is passed when f is called. Sometimes param becomes an lvalue reference, and sometimes it becomes an rvalue reference. param really is a universal reference.
参数是左值引用还是右值引用取决于在调用时调用方所传的实参。形参既可能是左值引用也可能是右值引用,形参是个forwarding reference。
有T&&的形式,有类型推导,那么&&表示一个forwarding reference。反过来说,如果没有类型推导,那么就不能说T&&是forwarding reference。
template<typename T>
void f(T&& param); // deduced parameter type ⇒ type deduction;
// && ≡ universal reference
template<typename T>
class Widget {
...
Widget(Widget&& rhs); // fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
template<typename T1>
class Gadget {
...
template<typename T2>
Gadget(T2&& rhs); // deduced parameter type ⇒ type deduction;
... // && ≡ universal reference
};
void f(Widget&& param); // fully specified parameter type ⇒ no type deduction;
// && ≡ rvalue reference
很简单,看到T&& (where T is a template parameter), T需要type deduction, 那么是个forwarding reference. 如果&&在一个具体类型后面,那么是个右值引用。
template<typename T>
void f(std::vector<T>&& param); // “&&” means rvalue reference
这里有&&,也有类型推导,但是形式不是T&&,而是std::vector
forwarding reference只存在于“T&&”形式中,即使加个const也不行。
template<typename T>
void f(const T&& param); // “&&” means rvalue reference
template<typename MyTemplateParamType>
void f(MyTemplateParamType&& param); // “&&” means universal reference
看个有T&&形式,但是没有类型推导的例子:
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
void push_back(T&& x); // fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
这里push_back的参数x是个右值引用。我们可以看下这个函数的类外定义形式:
template <class T>
void vector<T>::push_back(T&& x);
push_back 属于 std::vector
Widget makeWidget(); // factory function for Widget
std::vector<Widget> vw;
Widget w;
vw.push_back(makeWidget()); // create Widget from factory, add it to vw
push_back的调用会促使编译器实例化类std::vector
void std::vector<Widget>::push_back(Widget&& x);
看到没?类的实例化完成时,函数的参数类型已经确定了,不需要再推导了。
std::vector的emplace_back声明如下:
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
template <class... Args>
void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
... // && ≡ universal references
};
emplace_back的参数不受T影响,即使T确定下来,Args依然需要推导,类外声明:
template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);
所以emplace_back的参数是forwarding reference,push_back的参数是右值引用。
最后一点:一个expression是左值还是右值,跟这个expression的类型没关系。拿int来举例:声明为整形的变量,是个左值;字面值10,是右值;但他们都是整形。也就是说,左值还是右值这个概念跟类型没关系。所以,
- 一个左值,它的类型可以是右值引用
- 一个右值,它的类型可以时右值引用
Widget makeWidget(); // factory function for Widget
Widget&& var1 = makeWidget() // var1 is an lvalue, but
// its type is rvalue reference (to Widget)
Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
// its type is rvalue reference (to Widget)
用std::move可以方便的把一个左值转化为一个右值:
Widget var2 = std::move(var1); // equivalent to above
使用static_cast主要是为了显示,这个expression是个右值引用 (Widget&&).
有名字的变量跟参数都是左值(可以拿到他们的地址),即使他们的类型是右值引用:
template<typename T>
class Widget {
...
Widget(Widget&& rhs); // rhs’s type is rvalue reference,
... // but rhs itself is an lvalue
};
template<typename T1>
class Gadget {
...
template <typename T2>
Gadget(T2&& rhs); // rhs is a universal reference whose type will
... // eventually become an rvalue reference or
}; // an lvalue reference, but rhs itself is an lvalue
引用折叠
代码中如果有引用的引用,那么代码是无效的
Widget w1;
...
Widget& & w2 = w1; // error! No such thing as “reference to reference”
forwarding reference的模版参数的类型推导中,同样类型的左值跟右值,推导出来的类型是不一样的。
- 左值推导为T&
- 右值推导为T
考虑下,如果一个forwarding reference的模版参数,实例化时,分别用了左值跟右值,会有什么区别?
template<typename T>
void f(T&& param);
...
int x;
...
f(10); // invoke f on rvalue
f(x); // invoke f on lvalue
如果调用时用了右值10,那么推导出来的T是int,实例化后如下所述:
void f(int&& param); // f instantiated from rvalue
如果调用时用了左值x,那么推导出来的T是int&,实例化后如下所述:
void f(int& && param); // initial instantiation of f with lvalue
出现了引用的引用,按理说这样的代码是无效的,但是“f(x)”是完全合理的。c++11用了引用折叠(reference collapsing)来解决这个问题。
引用类型有两种,那么引用的引用类型就有四种:
- & &
- & &&
- && &
- && &&
右值引用的右值引用会变成右值引用,其他情况都会变成左值引用。
根据这个规则,上面的代码会变为:
void f(int& param); // instantiation of f with lvalue after reference collapsing
forwarding reference在进行类型推导前,会先对参数进行reference-stripping,也就是说去掉参数的引用部分,只留type,例子如下:
int x;
...
int&& r1 = 10; // r1’s type is int&&
int& r2 = x; // r2’s type is int&
f(r1);
f(r2);
r1跟r2的类型都是int&。r1,r2在类型推导前会剥离掉引用,只剩int类型,然后它们都是左值,那么就是int&了。
引用折叠除了在模版参数实例化有用到,在auto变量的定义中也有用到。他们的类型推导规则一样:
Widget&& var1 = someWidget; // var1 is of type Widget&& (no use of auto here)
auto&& var2 = var1; // var2 is of type Widget& (see below)
var1 是 Widget&&类型, 赋值给 var2时,引用部分被忽略了,只被当作 Widget类型. 因为是左值,推导出来的type是 Widget&. auto替换为 Widget& :
Widget& && var2 = var1; // note reference-to-reference
which, after reference collapsing, becomes
Widget& var2 = var1; // var2 is of type Widget&
第三个有引用折叠出现的场景是 typedef
template<typename T>
class Widget {
typedef T& LvalueRefType;
...
};
如下的代码中:
Widget<int&> w;
Widget包含下面的定义:
typedef int& & LvalueRefType;
引用折叠为
typedef int& LvalueRefType;
最后一个出现引用折叠的场景是:decltype.
跟auto一样,decltype推导出的类型有两种:T 或 T&,类型定了后,在用引用折叠规则。
跟auto不同的是,有名的非引用类型,会被推到为 T,同样的情况下,auto会推导为 T&
Widget w1, w2;
auto&& v1 = w1; // v1 is an auto-based universal reference being
// initialized with an lvalue, so v1 becomes an
// lvalue reference referring to w1.
decltype(w1)&& v2 = w2; // v2 is a decltype-based universal reference, and
// decltype(w1) is Widget, so v2 becomes an rvalue reference.
// w2 is an lvalue, and it’s not legal to initialize an
// rvalue reference with an lvalue, so
// this code does not compile.
总结:
forwarding reference 依赖两个规则:
- Reference collapsing(& always win)
- special type deduction for T&&(左值推导为T&,右值推导为T)