Rvalue Reference In Cxx11
author: vector03
mail: mmzsmm@163.com
1. copy constructor与临时对象.
当函数返回一个对象时,
如果CFoo的copy constructor如下,
代码片段,
考虑上面的情况, 对象foo在构造时, 将复制getFoo函数返回的临时对象里的内容.
假设CFoo内部存在大量资源, 后续会有一系列deep copy动作. 事实上, 当foo构造
结束后, 临时对象就没有任何意义了, 这是cxx对象构造时很难避免的一种低效初始
化方式.
2. move semantic
在cxx11中可以通过move语义, 将copy constructor替换为move constructor.
比如, CFoo的copy construct可以改写为如下,
通过move constructor, 在内部将source object的资源“偷”了过来.
move语义当然好, 但有些时候传统的copy construct还是需要的, 例如,
这种情况下, 使用move语义会引起麻烦, 因为你可能并不知道对象a在传递给b之后,
内部发生了不可见的变化. 如果后续再引用a, 就会非常危险.
而下面的情况, 则更加合理,
这里无论是从函数返回, 还是经过运算符重载后生成的对象都是匿名的, 临时的. 由于生命
周期很短, 因此转移其内部资源不失为一个明智之举.
对于之前的cxx编译器来说, 如何判断当前应该调用copy/move constructor是一个难题.
因为没有更好的手段去获知这一语义. 于是cxx11中增加了rvalue reference.
3. lvalue and rvalue
从字面上, 赋值运算符左边的表达式为lvalue, 右边的为rvalue. 多数情况下这是没有问题的,
但也有例外. 比如, 数组名虽然位于赋值运算左侧, 但却不能直接对一个数组名赋值, 因为它
是一个常量地址.
另一种lvalue和rvalue的定义是, lvalue指向一块内存空间, 并且可以获得指向该内存的
指针, 除此之外的值都是rvalue.
一般的, 两个对象的运算结果, 或者对象作为返回值, 可以看做是rvalue. 因为这种情况下
对象的地址往往保存在寄存器当中, 常规方法是无法获得其地址的.
4. rvalue reference
rvalue reference采用如下的形式T&&. 注意这并不是T&的引用, cxx中不存在双重引用的概念.
作为区分T&对应的是lvalue reference.
有了rvalue reference, 就可以分别实现copy/move constructor, compiler可以根据传入的
对象性质在编译阶段选择合适的重载.
另外, 在同时存在lvalue和rvalue可作为形参的情况下, 编译器会优先选择rvalue的版本.
前提是你已经实现了两种版本.
如果仅实现了CFoo(CFoo& foo), 则你只能使用lvalue.
如果实现了CFoo(CFoo const & foo), 同以前一样, 既可以使用lvalue也可以使用rvalue.
编译器将无法区分copy/move construct.
只有当同时实现了rvalue reference, 编译器才能正确overload不同的construct.
最后如果仅实现rvalue reference版本, 则你只能接收rvalue了.
5. 强制rvalue reference
有时候因为某些特殊需求, 我们可能希望对lvalue同样使用move语义(在明确自己在干什么的前提下),
cxx11中引入了std::move()来实现对lvalue的rvalue reference.
move()本身并不会转移什么, 事实上它的意义在于将对象moveable. 经过move()的作用, 一个lvalue
可以变成rvalue.
这种强制rvalue reference如同友元, 都是对当前体系的一种破坏, 带来方便和性能的同时, 带有一定
负面作用. 使用时需要额外当心.
6. rvalue reference是rvalue还是lvalue?
如果有如下的调用,
最终会打印, 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. 而如果这样修改,
则会将传入的形参foo强制为rvalue, 输出也就是另外一个结果了.
其子类假如这样继承,
这里实际上是编译不过去的. 本意是要调用父类的move constructor, 但由于传给Base的参数
已经非匿名了, rvalue reference实际上是一个lvalue, 因此, 将调用Base的lvalue版本. 由于并未
重载lvalue版本的Base constructor, 导致编译失败. 这里应该做如下修改,
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
......
}