关于C++的左值和右值,及右值引用和std::move、std::forward的意义.

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 SemanticPerfect 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赋值运算符”(即重载了右值的)的特性做了限制;

参考:C++ Rvalue References Explained

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值