Rvalue references solve at least 2 problems:
1. Implementing move semantics;(移动语义有助于高效拷贝)
2. Perfect forwarding;(完美转发,是移动语义的有效保证)
【何为左值,何为右边值?】
- An lvalue is an expression that refers to a memory location and allows us to take the address of that memory location via the & operator.
- An rvalue is an expression that is not an lvalue.
【什么是移动语义?】
Some methods that take considerable effort to construct, clone, or destruct, as follow:
classical implementation : destruct owns + clone target
// copy assignment operator for X
X& X::operator=(X const & rhs)
{
// Destruct the resource that m_pResource refers to
delete m_pResource;
// Make a clone of what rhs.m_pResource refers to
// and attach the clone to m_pResource
m_pResource = new Resource(*rhs.pResource);
}
X foo(){return X();};
X a;
a = foo();
- clones the resource from the temporary returned by foo,
- destructs the resource held by x and replaces it with the clone,
- destructs the temporary and thereby releases its resource;
move semantics : swap pointer
// copy assignment operator for X
X& X::operator=(X & rhs)
{
// swap m_pResource and rhs.m_pResource
delete m_pResource;
m_pResource = rhs.m_pResource;
}
- Rather obviously, it would be ok, and much more efficient, to swap resource pointers;
- Note: It can be called on l-values ,but r-values;
【右值引用】
if X is any type, then X&& is called an rvalue reference to X;
class X
{
X& X::operator=(X& x); // lvalue reference overload
X& X::operator=(X&& x); // rvalue reference overload
}
X a,b;
a = b; // lvalue : calls operator=(X&)
a = X(); // rvalue : calls operator=(X&&)
------------------------------------------------------------
class X
{
X& X::operator=(const X& x);
}
// it can be called on l-values and r-values,
// but it is not possible to make it distinguish between l-values and r-values.
X a,b;
a = b; // lvalue : ok
a = X(); // rvalue : ok
------------------------------------------------------------
class X
{
X& X::operator=(X&& x);
}
X a,b;
a = b; // lvalue : error
a = X(); // rvalue : ok
template<typename _Ty>
void foo(_Ty& x);
template<typename _Ty>
void foo(_Ty&& x);
int a;
foo(a); // lvalue : call foo(_Ty& x);
foo(1); // rvalue : call foo(_Ty&& x);
------------------------------------------------------------
template<typename _Ty>
void foo(_Ty&& x);
int a;
foo(a); // lvalue : ok
foo(1); // rvalue : ok
【右值引用就是右值吗?】
class MyClass
{
public:
int* a;
MyClass()
{
a = new int[10];
}
MyClass(MyClass& x)
{
auto p = a;
a = x.a;
x.a = p;
}
MyClass(MyClass&& x)
{
auto p = a;
a = x.a;
x.a = p;
}
}
void foo(MyClass&& x)
{
MyClass anotherX = x; // "MyClass(MyClass& x)" whould be called
}
- Things that are declared as rvalue reference can be lvalues or rvalues. 即:“右值引用”所引用的“对象”可以是左值,也可以是右值;
- 区别就是:如果它(“对象”)有一个名称,那么它就是一个左值,否则,就是一个右值;
In the example above, the “x” that is declared as an rvalue reference has a name,so it is an lvalue;
MyClass&& foo();
MyClass x = foo(); // "MyClass(MyClass&& x)" would be called;
【移动语义和编译器的优化】
class MyClass
{
public:
MyClass()
{
printf("MyClass\n");
}
MyClass(MyClass&& rhs)
{
printf("MyClass&&\n");
}
};
MyClass foo()
{
MyClass tmp;
// ...
return tmp;
}
MyClass a = foo(); // output: "MyClass" 、 "MyClass&&"
观察foo函数,可能会觉得,tmp到返回值,会有一次拷贝;你可能会以下面这种实现,来优化它:
MyClass&& foo()
{
MyClass tmp;
// ...
return std::move(tmp); // 转为右值
}
MyClass&& a = foo(); // output: "MyClass"
乍一看,这好像是行得通的,可以通过下面一个例子来测试:
static uint64 counter = 0;
class MyValue
{
public:
long long a[10000];
MyValue() { counter++; }
MyValue(MyValue&& rhs) { counter++; }
};
MyValue getMyValue()
{
MyValue obj;
obj.a[0] += rand()%1000;
return obj;
}
MyValue&& getMyRValue()
{
MyValue obj;
obj.a[0] += rand()%1000;
return std::move(obj);
}
// main function
int64 idx = 0;
Timer tmr;
int64 num = 1000000
tmr.Begin();
while (idx++ <= num)
{
MyValue a = getMyValue();
a.a[0] += rand() % 1000;
}
std::cout << "MyValue 无右值引用,耗费 " << tmr.Get<Milliseconds>() << " 毫秒\n";
std::cout << "拷贝复制次数: " << counter << std::endl;
counter = 0;
idx = 0;
tmr.Begin();
while (idx++ <= num)
{
MyValue&& a = getMyRValue();
a.a[0] += rand() % 1000;
}
std::cout << "MyValue 有右值引用,耗费 " << tmr.Get<Milliseconds>() << " 毫秒\n";
std::cout << "拷贝复制次数: " << counter << std::endl;
output (Debug):
MyValue 无右值引用,耗费 2051 毫秒
拷贝复制次数: 2000002
MyValue 有右值引用,耗费 2021 毫秒
拷贝复制次数: 1000001
虽然在Debug模式下,好像是行得通的,并减少了拷贝次数;
但事实上,这样是无用的,在编译Release版本时,编译器会做一些优化;
output (Release):
MyValue 无右值引用,耗费 40 毫秒
拷贝复制次数: 1000001
MyValue 有右值引用,耗费 38 毫秒
拷贝复制次数: 1000001
在本例子中,编译器不是在本地构造MyValue然后将其复制出来,而是直接在getMyValue()方法的返回值的位置构造MyValue对象。
显然,这比移动语义更好。
即:return value optimization and copy elision.
【完美转移】
问题:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
return shared_ptr<T>(new T(arg));
}
这个方法的目的就是:封装了智能指针的new方法;
很明显,参数以值传递,是非常低效的;
–> 改良1:
template<typename T, typename Arg>
shared_ptr<T> factory(const Arg& arg)
{
return shared_ptr<T>(new T(arg));
}
改为常量引用,左值与右值都能通过引用传递进来,但还是有问题:
- 阻碍了移动语意(move semantics),参数arg是一个左值;
–> 改良2:
在 C++ 11 以前,只能引用一个“变量”(忽略const &的特殊性),而无法引用一个“引用”;
C++ 11及以后,便出现以下这样的规则,“引用折叠规则”:
- (A&) & => A& (即一般引用的引用,解释成一般引用)
- (A&&) & => A&
- (A&) && => A&
- (A&&) && => A&&对于参数为右值引用的函数模板也有特殊的参数推导规则:
template<typename T> void foo(T&&);
- 当传递给foo的参数为A类型左值时,那么T被解释为:A& ;同时,通过引用折叠的规则,实际上参数变换为A&(T&& => (A&) && => A&);
- 当传递给foo的参数为A类型右值时,那么T被解释为A;同时,参数变换为A&&;
那么,根据这些规则,改良上面的方案:
namespace std
{
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return (static_cast<_Ty&&>(_Arg));
}
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type&& _Arg) noexcept
{ // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
return (static_cast<_Ty&&>(_Arg));
}
}
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
当调用factory时的参数推导过程:
X a;
factory<MyClass>(a);
左值版本:
shared_ptr<MyClass> factory((X &)&& arg)
{
return shared_ptr<MyClass>(new MyClass(std::forward<X&>(arg)));
}
===> arg
=> X& arg
constexpr (X &) && forward(
typename remove_reference<X &>::type& _Arg) noexcept
{
return (static_cast<(X &) &&>(_Arg));
}
===> _Arg
=> X& _Arg
===> return => X&
右值版本:factory<MyClass>(X());
shared_ptr<MyClass> factory((X &&)&& arg)
{
return shared_ptr<MyClass>(new MyClass(std::forward<X&&>(arg)));
}
===> arg
=> X&& arg
constexpr (X &&) && forward(
typename remove_reference<(X &&)>::type& _Arg) noexcept
{
return (static_cast<(X &&) &&>(_Arg));
}
===> _Arg
=> X& _Arg
===> return => X&&
为什么forward调用的不是
forward(typename remove_reference<_Ty>::type&& _Arg)
版本的?
“右值引用”所引用的“对象”可以是左值,也可以是右值;区别就是:如果它(“对象”)有一个名称,那么它就是一个左值,否则,就是一个右值;
来一个关于Move Semantic
和 Perfect Forwarding
的总结
class MyArg
{
public:
long long a[10000];
MyArg() { }
MyArg(const MyArg& rhs)
{
// 这里做一些 Copy 操作
std::cout << "MyArg& 左值引用"<<std::endl;
}
MyArg(MyArg&& rhs)
{
// 这里做一些 Move 操作
std::cout << "MyArg&& 右值引用"<<std::endl;
}
};
class MyClass
{
public:
MyClass(MyArg arg) {}
};
template<typename T, typename Arg>
std::auto_ptr<T> factory( Arg && arg)
{
return std::auto_ptr<T>(new T(std::forward<Arg>(arg)));
}
void testPerfectForwarding()
{
MyArg arg;
factory<MyClass>(arg); // 左值版本
factory<MyClass>(MyArg()); // 右值版本
}
OutPut :
> “MyArg& 左值引用”
> “MyArg&& 右值引用”
这便是一个“完美转发”:factory通过两个间接层将参数传递给MyClass构造器;
- 当传入的参数为左值,最终调用的是左值版本的拷贝构造函数;
- 当传入的参数为右值,最终调用的是右值版本的拷贝构造函数,那么此处,就可实现“Move Semantics”了;
【一些总结】
- “std::move”: pass its argument right through by reference and make it bind like an rvalue
- “std::forward”: forward the information whether at the call site, the wrapper saw an lvalue
- “no-name”规则: Things that are declared as rvalue reference can be lvalues or rvalues.If it has a name, then it is an lvalue. Otherwise, it is an rvalue.
- Reference Collapsing 规则: 见上文;
Function Template Argument Deduction 规则: 见上文;
移动语义通常只会在两个对象之间交换“指针” 和 “资源句柄”;
- 移动语义一般实现在:重载拷贝构造函数 和 重载类赋值运算符 上;
- 重写地 拷贝构造函数 和 类赋值运算 后面给它加上”noexcept”;
- 关于“Implicitly Moveing隐含移动”的问题,C++标准委员会对编译器生成“Move拷贝构造器”和“Move赋值运算符”(即重载了右值的)的特性做了限制;