当前位置:   article > 正文

C++的move语义真的没那么可怕啦_c++move ptr啥意思

c++move ptr啥意思

前言

To move or not to move: that is the question.
  ― Bjarne Stroustrup, 2010
  • 1
  • 2

作为C++11的新特性,move语义move semantics)和右值引用(rvalue reference)经常被放在一起讨论,在网络上也能看见不少的分析文章。但是使用右值引用这种全新的概念去解释move语义,其实会让人觉得门槛很高,似懂非懂,至少对于我来说是这样的。
因此本文尽量避免使用右值引用来解释move语义,并说明它在实际编程中的用法。本文只用C++标准库中的函数作为例子讲解,如何自己编写一个适用于move语义的类库不是考虑范围之内(因为这涉及到右值引用)。因为我相信如果一个连轮子都不会用的人是不可能造出好轮子的。

1.move语义再思考

首先我们先把这个词根据语法拆分成move语义来考虑,先来说明语义,再来说明move

1.1.语义 vs. 语法

更加简单直白地说。

  • 语法 = 某个源代码怎么写
  • 语义 = 这个源代码怎么执行
b = a[4];
// 同
b = *(a + 4);
  • 1
  • 2
  • 3

这里对于同一个语义,有两种不一样的语法可以用。一般我们会选择前一种来编写程序,但这不过是一种“语法糖”。本来使用后一种写法就足够了,但从阅读和编写的角度来说后一种的写法增加了理解的难度,因此增加了前一种写法(应该叫语法)。这里举这个例子只是为了说明语法和语义的关系并不是1:1的关系。

1.2.复制 vs. 移动

move相对的就是copy。众所周知,C++的变量都是值语义的。所谓的值语义的变量,就是它的行为跟int类型是一样的,典型表现就是作为参数传入函数中需要再复制一个作为形式参数,它的变化不会影响到原来的变量。
这里,我们再回到刚才谈到的语法/语义的关系,对于u=t这样的语法,实际上的语义是把变量t的内容复制到变量u当中。

1.3.move的两个含义

刚才提到的值语义的实现,本质上是依赖于复制的。但如果只用值语义来实现这样一个函数——将数组内的元素都乘以2,结果将会是下面这样的。

std::vector<int> twice_vector(std::vector<int>);  // 将数组内所用元素都乘以2然后再返回

std::vector<int> v = { /*很多元素*/ };
std::vector<int> w = twice_vector( v ); 
// v变量将在以后都不会被用到
  • 1
  • 2
  • 3
  • 4
  • 5

上面这段代码发生了多次没有意义的copy,及损失了时间又损失了内存。当然,这里当然会有读者直截了当地提出——“那就使用引用类型啊,像std::vector<int>&,不就行了吗?”。如果你了解move语义,这里同样可以使用它。
move语义也用另外一个用武之地,例如智能指针unique_ptr,如果只有值语义,那必然报错,因为这会造成重复释放对象。但通过move语义可以实现所有权的移动,参考下面的代码。

boost::unique_ptr<int> p( new int(42) );
boost::unique_ptr<int> q = p;  // 禁止复制!(编译会报错)

boost::unique_ptr<int> q ← p;  // 允许move(这里只是用'←'暂时表示move而已,实际语法并非这样)
assert(*q==42 && p.get()==NULL);
  • 1
  • 2
  • 3
  • 4
  • 5

综上所述move有两个含义——①以极低的性能消耗实现复制;②所有权的转移

1.4.C++11中的move语义

这里所说的move语义就是上面所说move两个含义的后者,翻译成代码就是:

int *p = new int(42);  // 指针p拥有值42的所有权
int *q;

// 值42的所有权从p转移到q
q = p;
p = NULL;
assert(*q==42 && p==NULL);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

因此这里的move语义就是把旧指针的值复制到新指针,并把旧指针的值赋为NULL。既然是这么简单的事情,为什么C++11还要把它作为一个新特性呢?原因有很多,①避免程序员所有权转移不彻底(忘了写后面的p=NULL);②让move这个语义更清晰;③增加语法支持(也就是常说的右值引用)让编译器在编译时就能做优化处理……

2.move语义的使用

这里举C++11的标准库作为例子说明。

2.1.C++11标准库和move

现在的C++11标准库基本上都增加了move语义的支持,不光是上面提到的std::unique_ptrC++03其实的std::stringstd::vector这两个我们最熟悉的类库同样支持。

std::string s1 = "apple";
std::string s2 = "banana";

s1 = std::move(s2);  //从s2转移到s1,关于s2的转移后的状态放在后面讨论
// s1=="banana"

std::vector<int> v1;
std::vector<int> v2 = {1, 2, 3, 4};

v1 = std::move(v2);  //从v2转移到v1
// v1=={1, 2, 3, 4}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
std::string s1 = "apple";
std::string s2 = "banana";

std::vector<std::string> dict;

dict.push_back( s1 );             // 通过复制s1的值来添加元素
// s1 == "apple"
dict.push_back( std::move(s2) );  // 通过移动s2来添加元素
// (关于s2的转移后的状态放在后面讨论)

std::vector<std::string> v;
v = std::move(dict);  // 整个容器移动实现复制
// v[0]=="apple" && v[1]=="banana"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.2.移动之后的状态?

如果要找出十分准确的答案,其实是挺麻烦的。C++11的标准类库的说法就是——“仍然有效,但状态不明”,实际上一般情况下为空,但并不能保证。以string为例说明。

std::string t = "xmas", u;
u = std::move(t);

// OK: t = "X";  再赋值是没有问题的,因为仍然有效
// OK: t.size() ; 求大小也没问题,但不能保证得到是什么
// NG: t[2];     有可能报错,因为有可能是空的
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但是如果对于某个类库,move语义表示的是所有权的移动这个含义,而不是以极低的性能消耗实现复制(参考1.3中两个含义),则这个类库必须保证转移后的状态必须为空,还是举unique_ptr为例子。

std::unique_ptr<int> p1( int new(42) );
std::unique_ptr<int> p2;

p2 = std::move(p1); 
// p1不再拥有所有权,保证 p1.get()==NULL必然成立
  • 1
  • 2
  • 3
  • 4
  • 5

2.3利用move语义编写高效函数

回到1.3一开始提到的那个问题,编写一个函数实现将数组内的元素都乘以2。接下来就可以通过move语义来高效实现了。

typedef std::vector<int> IntVec;
IntVec twice_vector(IntVec a)
{
  for (auto& e : a)
    e *= 2;
  return std::move(a); 
}

IntVec v = { /*...*/ };
IntVec w = twice_vector( std::move(v) );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/640095
推荐阅读
相关标签
  

闽ICP备14008679号