赞
踩
To move or not to move: that is the question.
― Bjarne Stroustrup, 2010
作为C++11
的新特性,move语义
(move semantics
)和右值引用
(rvalue reference
)经常被放在一起讨论,在网络上也能看见不少的分析文章。但是使用右值引用
这种全新的概念去解释move语义
,其实会让人觉得门槛很高,似懂非懂,至少对于我来说是这样的。
因此本文尽量避免使用右值引用
来解释move语义
,并说明它在实际编程中的用法。本文只用C++
标准库中的函数作为例子讲解,如何自己编写一个适用于move语义
的类库不是考虑范围之内(因为这涉及到右值引用
)。因为我相信如果一个连轮子都不会用的人是不可能造出好轮子的。
首先我们先把这个词根据语法拆分成move
和语义
来考虑,先来说明语义
,再来说明move
。
更加简单直白地说。
b = a[4];
// 同
b = *(a + 4);
这里对于同一个语义,有两种不一样的语法可以用。一般我们会选择前一种来编写程序,但这不过是一种“语法糖”。本来使用后一种写法就足够了,但从阅读和编写的角度来说后一种的写法增加了理解的难度,因此增加了前一种写法(应该叫语法)。这里举这个例子只是为了说明语法和语义的关系并不是1:1的关系。
与move
相对的就是copy
。众所周知,C++
的变量都是值语义
的。所谓的值语义
的变量,就是它的行为跟int
类型是一样的,典型表现就是作为参数传入函数中需要再复制一个作为形式参数,它的变化不会影响到原来的变量。
这里,我们再回到刚才谈到的语法/语义的关系,对于u=t
这样的语法,实际上的语义是把变量t
的内容复制到变量u
当中。
刚才提到的值语义
的实现,本质上是依赖于复制的。但如果只用值语义来实现这样一个函数——将数组内的元素都乘以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
有两个含义——①以极低的性能消耗实现复制;②所有权的转移
这里所说的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
这个语义更清晰;③增加语法支持(也就是常说的右值引用)让编译器在编译时就能做优化处理……
这里举C++11
的标准库作为例子说明。
现在的C++11
标准库基本上都增加了move语义
的支持,不光是上面提到的std::unique_ptr
,C++03
其实的std::string
和std::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"
如果要找出十分准确的答案,其实是挺麻烦的。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必然成立
回到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) );
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。