C++的move语义真的没那么可怕啦

本文深入探讨了C++11中的Move语义,包括其两种含义:以低性能消耗实现复制和所有权的转移。并通过标准库中的示例说明了Move语义的实际应用,如std::move函数的使用及其效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

作为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: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变量将在以后都不会被用到

上面这段代码发生了多次没有意义的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);

综上所述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);

因此这里的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}
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"

2.2.移动之后的状态?

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

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

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

但是如果对于某个类库,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必然成立

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) );
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值