[转] C++11带来的move语义

时间:2023-03-09 03:28:07
[转] C++11带来的move语义

PS: 通过引入接收右值的函数形参,可以通过接收右值来实现高效

PS在C++11中,标准库在<utility>中提供了一个有用的函数std::move,这个函数的名字具有迷惑性,因为实际上 std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。从实现上 讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

C++ 11带来了move语义,可以有效的提高STL的效率,这篇文章写的非常好,可以参考,这里对原文进行翻译,加入我自己的理解

原文:http://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html

先看看这个例子:

  1. #include <iostream>
  2. using namespace std;
  3. vector<int> doubleValues (const vector<int>& v)
  4. {
  5. vector<int> new_values( v.size() );
  6. for (auto itr = new_values.begin(), end_itr = new_values.end(); itr != end_itr; ++itr )
  7. {
  8. new_values.push_back( 2 * *itr );
  9. }
  10. return new_values;
  11. }
  12. int main()
  13. {
  14. vector<int> v;
  15. for ( int i = 0; i < 100; i++ )
  16. {
  17. v.push_back( i );
  18. }
  19. v = doubleValues( v );
  20. }

调用函数doubleValues时是有两次复制的,一次是在函数返回时,因为local变量要被回收,所以需要copy构造一个临时对象来返回,当返回这个临时对象后,又需要一次copy赋值操作来赋值给v。你可能会说,不返回对象就可以啊,可以返回一个引用或者一个指针,但是要这样做就得分配内存,但是C++的一个设计目标就是尽量少分配内存。上述更糟糕之处在于,返回的临时对象使用完之后是会被销毁掉的。

理论上来说,在内存中我们完全可以把临时对象的指针偷过来,而不去拷贝。还有就是我们为什么不能move呢?C++03说,我不知道哪个对象是临时的,哪个不是临时的,但是C++11却可以做到。

这就是右值引用和move语义要做的事情:move语义可以使你在使用临时对象时避免拷贝,并且可以安全的使用临时对象里的资源。

左值和右值

C++中,左值是指可以使用其地址的表达式,左值提供一种(半)永久的内存,比如:

int a;

a = 1;  //a是一个左值

又如:

  1. intx;
  2. int& getRef ()
  3. {
  4. returnx;
  5. }
getRef()
= 4;//getRef()这个表达式也是一个左值,因为其提供的返回值全局变量,是有固定地址的。
右值呢?如果一个表达式的结果在一个临时变量中,那它就是一个右值,例如:
  1. intx;
  2. intgetVal ()
  3. {
  4. returnx;
  5. }
  6. getVal();

再来看返回对象的情况:

  1. string getName ()
  2. {
  3. return"Alex";
  4. }
getName();
string
name = getName();

此时,对“Alex”进行隐式类型转换,转换为一个string的临时对象,然后拷贝构造给name变量

上边说到C++11能检测出来临时变量和非临时变量,现在就来看看怎么检测

既然有的表达式返回临时变量,那么如果对表达式重载,那么一个返回临时变量的表达式和返回非临时变量的表达式是否有哪些不同呢?

conststring&
name = getName();
//
ok

string&
name = getName();
//
NOT ok

为什么第一个ok第二个不ok呢,C++认为不管getName如何实现,如果你返回的是一个非const引用,说明你可能回去修改一个可能会消失

(如果是一个临时变量的话)东西;

另外,如果是返回一个const引用,确保了临时变量不会很快消失(不是很懂,不会很快消失,那多久会消失呢)。

C++11中,会让你可以绑定一个mutable右值引用,也就是说,一个值是否临时的,可以使用右值引用来检测,右值引用使用&&语法,可以是const也可以是非const的:

conststring&&
name = getName();
//
ok

string&&
name = getName();
//
also ok - praise be!

有什么用呢?关于左值引用和右值引用,最重要的是,当你写一个函数,函数的参数是左值引用或者右值引用时,比如:

[cpp] view plaincopy

  1. printReference (constString& str)
  2. {
  3. cout << str;
  4. }
  5. printReference (String&& str)
  6. {
  7. cout << str;
  8. }

第一个具有const引用的函数,可以接受任意的参数,不管是左值还是右值,不管这个左值或者右值易变或者不易变(if mutable);

但是对于第二个函数,除了mutable rvalue-references类型,其他的类型都可以,比如:

[cpp] view plaincopy

  1. string me( "alex");//mutable rvalue-references类型
  2. printReference(  me ); // calls the first printReference function, taking an lvalue reference

如果是printReference("alex");是不是就调用的第二个函数?

现在,右值引用版本的函数就像一个俱乐部的入口,只有临时变量才可以进入。

现在,既然有方法检测出是否临时变量了,有什么用处呢?

move构造和move赋值操作符
右值引用的一个用处是可以用来创建move构造函数和move赋值操作符,move构造和move赋值可以避免内存重新分配,
因为我们已经有了一个临时变量了。
把一个对象中的一个字段移动到另一个对象是什么意思?如果是一个原生类型,比如int,我们赋值就可以了,
但是对于指针类型的处理比较有趣。我们不需要再
分配和初始化一段内存,我们只需要把临时对象中的指针偷过来,然后让原始指针指向null即可,
因为临时对象不再需要了,所以我们可以把它的指针拿过来
用:
来看拷贝构造:
  1. class ArrayWrapper
  2. {
  3. public:
  4. ArrayWrapper (int n)
  5. : _p_vals( new int[ n ] )
  6. , _size( n )
  7. {}
  8. // copy constructor
  9. ArrayWrapper (const ArrayWrapper& other)
  10. : _p_vals( new int[ other._size  ] )
  11. , _size( other._size )
  12. {
  13. for ( int i = 0; i < _size; ++i )
  14. {
  15. _p_vals[ i ] = other._p_vals[ i ];
  16. }
  17. }
  18. ~ArrayWrapper ()
  19. {
  20. delete [] _p_vals;
  21. }
  22. private:
  23. int *_p_vals;
  24. int _size;
  25. };

来看move构造:

  1. class ArrayWrapper
  2. {
  3. public:
  4. // default constructor produces a moderately sized array
  5. ArrayWrapper ()
  6. : _p_vals( new int[ 64 ] )
  7. , _size( 64 )
  8. {}
  9. ArrayWrapper (int n)
  10. : _p_vals( new int[ n ] )
  11. , _size( n )
  12. {}
  13. // move constructor
  14. ArrayWrapper (ArrayWrapper&& other)
  15. : _p_vals( other._p_vals  )
  16. , _size( other._size )
  17. {
  18. other._p_vals = NULL;
  19. }
  20. // copy constructor
  21. ArrayWrapper (const ArrayWrapper& other)
  22. : _p_vals( new int[ other._size  ] )
  23. , _size( other._size )
  24. {
  25. for ( int i = 0; i < _size; ++i )
  26. {
  27. _p_vals[ i ] = other._p_vals[ i ];
  28. }
  29. }
  30. ~ArrayWrapper ()
  31. {
  32. delete [] _p_vals;
  33. }
  34. private:
  35. int *_p_vals;
  36. int _size;
  37. };

可以看出,move构造和copy构造相互重载了,那么调用的时候如何判断调用哪个构造呢?很简单,上边不是已经有了方法吗,

如果是临时对象就会自动调用move构造,如果不是,就会调用copy构造。
那么为什么move构造中要把原始指针置为null呢?因为如果不置为null,那么临时变量消失时,会析构,释放自己的指针,
还资源给os,那么新对象
中的指针也就随之会被delete掉。所以需要把原始指针置为null,这样临时变量在析构时就找不到给自己分配的那块内存了,
那块内存也就可以被新对象
正常使用了。

注意:这里的重载规则要求如果要调用move构造,那么一定要传一个临时变量,并且一定要是一个可以修改的临时变量