Rvalue Reference In Cxx11

Rvalue Reference In Cxx11

author: vector03
mail:   mmzsmm@163.com


1. copy constructor与临时对象.

当函数返回一个对象时,
CFoo getFoo(args...) {
    CFoo ret(...);
    ...
    ...
    return ret;
}

如果CFoo的copy constructor如下,
CFoo::CFoo(const CFoo& foo) {
        // resource copy
        ...
}

代码片段,
{
    ...
    CFoo foo(getFoo(...));
    ...
}

考虑上面的情况, 对象foo在构造时, 将复制getFoo函数返回的临时对象里的内容.
假设CFoo内部存在大量资源, 后续会有一系列deep copy动作. 事实上, 当foo构造
结束后, 临时对象就没有任何意义了, 这是cxx对象构造时很难避免的一种低效初始
化方式.


2. move semantic

在cxx11中可以通过move语义, 将copy constructor替换为move constructor.

比如, CFoo的copy construct可以改写为如下,
    CFoo::CFoo(const CFoo& foo) {
        // 1. Release the resource the object already had.
        // 2. Copy the resource handle from the source to the target object.
        // 3. Set resource handle of the source object to NULL.

    }

通过move constructor, 在内部将source object的资源“偷”了过来.

move语义当然好, 但有些时候传统的copy construct还是需要的, 例如,
    CFoo a();

    CFoo b(a);

这种情况下, 使用move语义会引起麻烦, 因为你可能并不知道对象a在传递给b之后,
内部发生了不可见的变化. 如果后续再引用a, 就会非常危险.

而下面的情况, 则更加合理,
    CFoo a(getFoo());


或者,

    CFoo a();
    CFoo b();
    CFoo c(a + b);

这里无论是从函数返回, 还是经过运算符重载后生成的对象都是匿名的, 临时的. 由于生命
周期很短, 因此转移其内部资源不失为一个明智之举.

对于之前的cxx编译器来说, 如何判断当前应该调用copy/move constructor是一个难题.
因为没有更好的手段去获知这一语义. 于是cxx11中增加了rvalue reference.


3. lvalue and rvalue

从字面上, 赋值运算符左边的表达式为lvalue, 右边的为rvalue. 多数情况下这是没有问题的,
但也有例外. 比如, 数组名虽然位于赋值运算左侧, 但却不能直接对一个数组名赋值, 因为它
是一个常量地址.

char name[] = "foo";  //ok
char* buff = "foo";
name = buff;          //err, 数组名不能赋值.


另一种lvalue和rvalue的定义是, lvalue指向一块内存空间, 并且可以获得指向该内存的
指针, 除此之外的值都是rvalue.

// lvalue sample
int a = 10;     //ok, a是lvalue
int* p = &a;    //ok, 可以获得a的地址
int& foo();
foo() = 10;     //ok, foo()是lvalue
int* p = &foo();    //ok, 可以获得lvalue地址

// rvalue sample
int a = 10;
int b = 3;
a * b = 15;     //err, a * b是rvalue
int bar();
int* p1 = &bar();    //err, bar()是rvalue, 不能获得地址
bar() = a;      //err, 同上, 不能对rvalue赋值


一般的, 两个对象的运算结果, 或者对象作为返回值, 可以看做是rvalue. 因为这种情况下
对象的地址往往保存在寄存器当中, 常规方法是无法获得其地址的.


4. rvalue reference

rvalue reference采用如下的形式T&&. 注意这并不是T&的引用, cxx中不存在双重引用的概念.
作为区分T&对应的是lvalue reference.

有了rvalue reference, 就可以分别实现copy/move constructor, compiler可以根据传入的
对象性质在编译阶段选择合适的重载.

CFoo::CFoo(CFoo const & foo) {
    // impliment copy construct
    ......
}

CFoo::CFoo(CFoo&& foo) {
    // impliment move construct
    ......
}

CFoo foo;
CFoo foo1(foo);      // 调用copy construct
CFoo getFoo();
CFoo foo2(getFoo()); // 调用move construct


另外, 在同时存在lvalue和rvalue可作为形参的情况下, 编译器会优先选择rvalue的版本.
前提是你已经实现了两种版本.

如果仅实现了CFoo(CFoo& foo), 则你只能使用lvalue.
CFoo(CFoo& foo);
CFoo foo;
CFoo foo1(foo);     //ok, foo是lvalue
CFoo foo2(CFoo());  //error, CFoo()是rvalue


如果实现了CFoo(CFoo const & foo), 同以前一样, 既可以使用lvalue也可以使用rvalue.
编译器将无法区分copy/move construct.
CFoo(CFoo const & foo);
CFoo foo;
CFoo foo1(foo);     //ok, foo是lvalue
CFoo foo2(CFoo());  //ok, 可以接收rvalue


只有当同时实现了rvalue reference, 编译器才能正确overload不同的construct.
CFoo(CFoo const & foo);
CFoo(CFoo&& foo);
CFoo foo;
CFoo foo1(foo);     //使用CFoo(const Foo& foo)
CFoo foo2(CFoo());  //使用CFoo(CFoo&& foo)


最后如果仅实现rvalue reference版本, 则你只能接收rvalue了.
CFoo(CFoo&& foo);
CFoo foo;
CFoo foo2(foo);     //err, foo是lvalue

5. 强制rvalue reference

有时候因为某些特殊需求, 我们可能希望对lvalue同样使用move语义(在明确自己在干什么的前提下),
cxx11中引入了std::move()来实现对lvalue的rvalue reference.

move()本身并不会转移什么, 事实上它的意义在于将对象moveable. 经过move()的作用, 一个lvalue
可以变成rvalue.

CFoo(CFoo&& foo);
CFoo foo;       //foo是lvalue
CFoo foo1(foo)  //err, 不能接收lvalue
CFoo foo1(std::move(foo));  //ok, move后lvalue成为rvalue. 执行后foo的内容发生了变化.


这种强制rvalue reference如同友元, 都是对当前体系的一种破坏, 带来方便和性能的同时, 带有一定
负面作用. 使用时需要额外当心.


6. rvalue reference是rvalue还是lvalue?

如果有如下的调用,

CFoo::CFoo() {}

CFoo::CFoo(CFoo const & foo) {
    cout << "lvalue copy construct!" << endl;
}

CFoo::CFoo(CFoo&& foo) {
    cout << "rvalue copy construct!" << endl;
}

void testFoo(CFoo&& foo) {
    CFoo foo1(foo);
}

int main(int argc, char **argv) {
    CFoo foo();
    CFoo foo1(std::move(foo));
    return 0;
}   

最终会打印, lvalue copy construct还是rvalue copy construct呢?
答案是执行结果是前者.

但我明明用move语义将foo强制为rvalue了, 为什么最后执行的是lvalue呢? 这是因为cxx标准委员会在
设定rvalue reference时, 对于其定义为, rvalue reference既是lvalue也是rvalue(或者称其为xvalue)
具体是哪一种要看rvalue reference引用的对象是否匿名. 如果其是匿名的, 则rvalue reference
表现为rvalue, 否则为lvalue.

上面的demo中, testFoo虽然接受CFoo&&类型的参数, 但当其传递给CFoo foo1对象时, 由于其不是匿名
的, 因此, 表现为lvalue. 最终将调用CFoo的lvalue版本copy construct. 而如果这样修改,

void testFoo(CFoo&& foo) {
    CFoo foo1(std::move(foo));
}


则会将传入的形参foo强制为rvalue, 输出也就是另外一个结果了.

类似的例子还有父类和子类之间. 如果存在这样的父类,

Base(Base&& o);

其子类假如这样继承,

Derived(Derived && o) : Base(o) {
    // Derived specific
    ......
}

这里实际上是编译不过去的. 本意是要调用父类的move constructor, 但由于传给Base的参数
已经非匿名了, rvalue reference实际上是一个lvalue, 因此, 将调用Base的lvalue版本. 由于并未
重载lvalue版本的Base constructor, 导致编译失败. 这里应该做如下修改,

Derived(Derived&& o) : Base(std::move(o)) {
    // Derived specific
    ......
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值