如果说新的语言特性使得过去的最佳实践不再成立的话,我想move构造函数和std::move所代表的move语义应该算其中一个。
在解释move引起的变化之前,这里先定义一个支持自定义move操作的类
C++
class Foo {
public:
explicit Foo(int value) : value_{value} {
std::cout << "Foo(int)\n";
}
// copy
Foo(const Foo &foo) : value_{foo.value_} {
std::cout << "Foo(copy)\n";
}
// copy assignment
Foo &operator=(const Foo &foo) = delete;
// move
Foo(Foo &&foo) {
std::cout << "Foo(move)\n";
value_ = foo.value_;
foo.value_ = 0;
}
// move assignment
Foo &operator=(Foo &&foo) {
std::cout << "Foo(move assignment)\n";
value_ = foo.value_;
foo.value_ = 0;
return *this;
}
int value() const {
return value_;
}
void set_value(int value) {
value_ = value;
}
~Foo() {
std::cout << "~Foo(" << value_ << ")\n";
}
friend std::ostream &operator<
os << "Foo(" << foo.value_ << ")";
return os;
}
private:
int value_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
classFoo{
public:
explicitFoo(intvalue):value_{value}{
std::cout<
}
// copy
Foo(constFoo&foo):value_{foo.value_}{
std::cout<
}
// copy assignment
Foo&operator=(constFoo&foo)=delete;
// move
Foo(Foo&&foo){
std::cout<
value_=foo.value_;
foo.value_=0;
}
// move assignment
Foo&operator=(Foo&&foo){
std::cout<
value_=foo.value_;
foo.value_=0;
return*this;
}
intvalue()const{
returnvalue_;
}
voidset_value(intvalue){
value_=value;
}
~Foo(){
std::cout<
}
friendstd::ostream&operator<
os<
returnos;
}
private:
intvalue_;
};
注意构造函数 Foo(Foo&& foo) ,和一般的reference的标记 & 不同,这里有两个 & 符号。其次,参数没有加const。
move构造函数要做的事情,是把输入参数所拥有的内容移动到自己的实例中,类似数据所有权转移。具体来说
C++
Foo f1{1};
Foo f2 = std::move(f1};
// f1 becomes Foo{0}
// f2 becomes Foo{1}
1
2
3
4
Foof1{1};
Foof2=std::move(f1};
// f1 becomes Foo{0}
// f2 becomes Foo{1}
上述代码中,f1的数据被转移到了f2中,f1中的数据不再可用(这里被置为0)。
这样做有什么用呢?第一个想到的就是代码中数据所有权的转移。
在给出所有权转移的例子之前,复习一下C++中的copy和borrow。
C++
void with_operation1(Foo foo) {
}
void with_operation2(Foo &foo) {
std::cout << foo.value() << std::endl;
foo.set_value(2);
}
void with_operation3(const Foo &foo) {
std::cout << foo.value() << std::endl;
// foo.set_value(2);
}
int main() {
Foo foo{1};
// copy
with_operation1(foo);
// borrow
with_operation2(foo);
// borrow
with_operation3(foo);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
voidwith_operation1(Foofoo){
}
voidwith_operation2(Foo&foo){
std::cout<
foo.set_value(2);
}
voidwith_operation3(constFoo&foo){
std::cout<
// foo.set_value(2);
}
intmain(){
Foofoo{1};
// copy
with_operation1(foo);
// borrow
with_operation2(foo);
// borrow
with_operation3(foo);
return0;
}
with_operation1中Foo会被复制,对于其他语言背景的人来说,这是必须了解和注意的。
with_operation2和with_operation3中Foo不会被复制,根据是否有const来决定是否可以修改参数中的Foo。
事情看起来很完美?似乎没有引入move语义的必要?
考虑一个FooWrapper
C++
class FooWrapper {
public:
explicit FooWrapper(Foo&& foo): foo_{std::move(foo)} {}
private:
Foo foo_;
};
int main() {
Foo foo{1};
FooWrapper wrapper{std::move(foo)};
// wrapper.foo_ moved here
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
classFooWrapper{
public:
explicitFooWrapper(Foo&&foo):foo_{std::move(foo)}{}
private:
Foofoo_;
};
intmain(){
Foofoo{1};
FooWrapperwrapper{std::move(foo)};
// wrapper.foo_ moved here
return0;
}
在构造了Foo之后,需要把所有权转给FooWrapper。
在没有move之前,你可能会考虑指针。但是有了move之后,你可以通过std::move间接转移数据内容达到所有权转移的效果。转移之后main函数内栈上构造的Foo也可以安全销毁。
如果你执行上述程序,可以得到以下结果
Foo(int)
Foo(move)
~Foo(1)
~Foo(0)
1
2
3
4
Foo(int)
Foo(move)
~Foo(1)
~Foo(0)
也就说,FooWrapper中的foo_和main函数中的foo是两个不同的变量,std::move触发了数据转移,间接达到了所有权转移。
这里为什么一直强调是数据所有权的转移呢?原因是变量本身没有被move,这点很重要。所以变量的析构函数仍旧会被调用。假如你的变量中包含指针的话,不能简单地复制一下就结束,需要置原变量的指针为nullptr。
举个例子
C++
class IntPointerHolder {
public:
explicit IntPointerHolder(int *ip) : ip_{ip} {}
// no copy
IntPointerHolder(const IntPointerHolder &) = delete;
// no copy assignment
IntPointerHolder &operator=(const IntPointerHolder &) = delete;
// move
IntPointerHolder(IntPointerHolder &&holder) {
ip_ = holder.ip_;
holder.ip_ = nullptr;
}
// no move assignment
IntPointerHolder &operator=(IntPointerHolder &&holder) = delete;
~IntPointerHolder() {
delete ip_;
}
private:
int *ip_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
classIntPointerHolder{
public:
explicitIntPointerHolder(int*ip):ip_{ip}{}
// no copy
IntPointerHolder(constIntPointerHolder&)=delete;
// no copy assignment
IntPointerHolder&operator=(constIntPointerHolder&)=delete;
// move
IntPointerHolder(IntPointerHolder&&holder){
ip_=holder.ip_;
holder.ip_=nullptr;
}
// no move assignment
IntPointerHolder&operator=(IntPointerHolder&&holder)=delete;
~IntPointerHolder(){
deleteip_;
}
private:
int*ip_;
};
注意move构造函数里设置holder.ip_为nullptr的地方。这是必须做的,否则两个变量(原变量和目标变量)的析构函数都会被调用,造成double free问题。
上述代码其实在标准库中对应有一个unique_ptr(C++11引入),可以达到完全一样的效果。unique_ptr不支持copy,只支持move,可以帮助你写出所有权唯一的代码。比如说
C++
class SimpleIntArray {
public:
explicit SimpleIntArray(size_t length): array_{new int[length]}, length_{length} {
for(int i = 0; i < length; ++i) {
array_[i] = 0;
}
}
void debug() {
std::cout << length_ << ' ';
if(array_) {
for(int i = 0; i < length_; i++) {
std::cout << array_[i] << ' ';
}
}
std::cout << std::endl;
}
private:
std::unique_ptr array_;
size_t length_;
};
int main() {
SimpleIntArray array1{3};
array1.debug();
SimpleIntArray array2 = std::move(array1);
array1.debug();
array2.debug();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
classSimpleIntArray{
public:
explicitSimpleIntArray(size_tlength):array_{newint[length]},length_{length}{
for(inti=0;i
array_[i]=0;
}
}
voiddebug(){
std::cout<
if(array_){
for(inti=0;i
std::cout<
}
}
std::cout<<:endl>
}
private:
std::unique_ptrarray_;
size_tlength_;
};
intmain(){
SimpleIntArrayarray1{3};
array1.debug();
SimpleIntArrayarray2=std::move(array1);
array1.debug();
array2.debug();
return0;
}
输出结果为
3 0 0 0
3
3 0 0 0
1
2
3
3 0 0 0
3
3 0 0 0
注意代码中没有定义move构造函数,编译器默认生成的move构造函数中会逐个move成员变量,不支持move操作的成员变量会回退到copy操作。
可以想到,如果SimpleIntArray直接操作指针,并且要处理数据所有权转移的话会是一件比较麻烦的事情。相比之下这里通过unique_ptr非常简单地实现了数据转移和指针管理。
这里注意一点,从输出来看,默认生成的move构造函数在处理length_的转移时,并没有置为0。严格来说,move之后的原数据,内部状态如何是不确定的。所以理论上不应该去调用。硬要解决的话,这里可以自定义实现move构造函数,写一个直接move的length类型或者编码实践要求。
回到之前FooWrapper的代码,可以看到有两个std::move,如果问是否可以改成一个,结论是可以,不过个人不推荐。这里重要的是理解std::move做了什么,为什么需要std::move。
C++
class FooWrapper {
public:
explicit FooWrapper(Foo&& foo): foo_{std::move(foo)} {}
private:
Foo foo_;
};
int main() {
Foo foo{1};
FooWrapper wrapper{std::move(foo)};
// wrapper.foo_ moved here
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
classFooWrapper{
public:
explicitFooWrapper(Foo&&foo):foo_{std::move(foo)}{}
private:
Foofoo_;
};
intmain(){
Foofoo{1};
FooWrapperwrapper{std::move(foo)};
// wrapper.foo_ moved here
return0;
}
FooWrapper wrapper{std::move(foo)}; 这行代码中,std::move是因为FooWrapper的构造函数的参数是 Foo&& ,换句话说要求是一个rvalue。这里不具体展开什么是rvalue。只要知道对于在栈上构造的foo来说,必须通过std::move转换为FooWrapper需要的Foo&&。
作为参考,临时的Foo可以不使用std::move
C++
FooWrapper wrapper{Foo{1}};
1
FooWrapperwrapper{Foo{1}};
可以看到,临时变量的情况下只有一个FooWrapper内部的std::move(具体编码中,如果不确定是否需要std::move,可以先不加,看编译器是否报错)。
因为外层的std::move只是起到转换为rvalue的作用,所以理论上不会触发move构造函数。事实上也是这样的,实际触发move构造函数的是 foo_{std::move(foo)} 这句。注意,这里如果不加 std::move,调用的会是copy构造函数。
小结一下
Wrapper的构造函数中使用std::move
调用Wrapper构造函数的地方看情况使用std::move,比如栈上分配的变量
老实说,要完全讲清楚什么时候用std::move必须完全理解rvalue,但是看 cppreference 上的定义一头雾水。所以个人觉得,常见pattern+自己试错可能是最好的。
C++11引入的move语义很重要的一个原因,个人认为是标准库增加了对于move的支持。你想利用好新版本的功能,而不是固守旧版本的最佳实践的话,有必要了解move带来的影响。本篇的最后,分析一下对于常规的函数输入和输出的影响。
首先是返回值
C++
Foo makeFoo() {
return Foo{1};
}
int main() {
// Foo(int)
// copy/move is omitted
Foo foo = makeFoo();
return 0;
}
1
2
3
4
5
6
7
8
9
10
FoomakeFoo(){
returnFoo{1};
}
intmain(){
// Foo(int)
// copy/move is omitted
Foofoo=makeFoo();
return0;
}
你可能没有看到std::move也没有看到move构造函数被调用,原因是编译器的“构造函数消除”优化启用了。如果你关闭了这个优化,可以看到move构造函数被调用。假如你禁用move构造函数的话,copy构造函数被调用。
顺便说一句,C++中的函数的返回值是否可以是一个对象?个人觉得,对于一个类似factory一样返回函数内栈上分配的对象的函数的话,由于“构造函数消除”优化的关系,和通过方法传入其实没有太大区别。即使没有“构造函数优化”,默认使用move而不是copy。但是如果你说的是异常处理,并且不想用C++默认的exception机制的话,那就是另外一回事情了。
回到move语义对函数的影响,之前的with_operation系列其实还有move版本
C++
void with_operation4(Foo&& foo) {
}
void with_operation5(Foo&& foo) {
foo.set_value(2);
Foo foo2 = std::move(foo);
std::cout << foo2 << std::endl;
}
int main() {
Foo f1{1};
Foo f2 = std::move(f1); // f1 become Foo(0) now
// with_operation4(f2);
with_operation4(std::move(f2));
with_operation5(std::move(f2));
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
voidwith_operation4(Foo&&foo){
}
voidwith_operation5(Foo&&foo){
foo.set_value(2);
Foofoo2=std::move(foo);
std::cout<
}
intmain(){
Foof1{1};
Foof2=std::move(f1);// f1 become Foo(0) now
// with_operation4(f2);
with_operation4(std::move(f2));
with_operation5(std::move(f2));
return0;
}
注意with_operation4虽然要求输入是Foo&&,但是函数内部没有通过std::move转移数据,所以main函数中的f2没有任何变化。相对的,with_operation5中转移了数据,所以f2数据不再有效。
考虑一个问题,假如你希望某个函数接管某个变量的数据所有权,该怎么定义函数?
答案其实很明显,上面的几段代码中都出现了,使用 T&& 这种形式
C++
void some_function(Foo&& foo) {
Foo bar = std::move(foo);
}
int main() {
Foo foo{1};
some_function(std::move(foo));
some_function(Foo{2});
}
1
2
3
4
5
6
7
8
9
voidsome_function(Foo&&foo){
Foobar=std::move(foo);
}
intmain(){
Foofoo{1};
some_function(std::move(foo));
some_function(Foo{2});
}
这里 some_function(Foo&) 肯定不行,对于 some_function(Foo{2}) 是无法编译通过的。
some_function(Foo)可以编译通过,但是 some_function(foo) 是复制, some_function(std::move(foo)) 调用时触发一次move,some_function中又move,结果有两次move。
综上所述,对于有数据转移要求的函数,使用 T&& 这种形式。
最后一个问题,对于builder这种类(Builder设计模式),如何定义build方法?
builder这种类,很典型的从类中向外数据转移。在了解了move对于返回值的影响之后,具体可以怎么写呢?
C++
class FooBuilder {
public:
explicit FooBuilder(int i): foo_{i} {}
Foo build() {
return std::move(foo_);
}
Foo& build2() {
return foo_;
}
Foo build3() {
return foo_;
}
Foo&& build4() {
return std::move(foo_);
}
private:
Foo foo_;
};
int main() {
FooBuilder builder1{1};
builder1.build().set_value(-1); // move, ok
FooBuilder builder2{2};
Foo foo2 = builder2.build(); // move, ok
FooBuilder builder3{3};
Foo foo3 = builder3.build2(); // copy
FooBuilder builder4{4};
Foo foo4 = builder4.build3(); // copy
FooBuilder builder5{5};
builder5.build4().set_value(-1); // not moved here
Foo foo5 = builder5.build4();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
classFooBuilder{
public:
explicitFooBuilder(inti):foo_{i}{}
Foobuild(){
returnstd::move(foo_);
}
Foo&build2(){
returnfoo_;
}
Foobuild3(){
returnfoo_;
}
Foo&&build4(){
returnstd::move(foo_);
}
private:
Foofoo_;
};
intmain(){
FooBuilderbuilder1{1};
builder1.build().set_value(-1);// move, ok
FooBuilderbuilder2{2};
Foofoo2=builder2.build();// move, ok
FooBuilderbuilder3{3};
Foofoo3=builder3.build2();// copy
FooBuilderbuilder4{4};
Foofoo4=builder4.build3();// copy
FooBuilderbuilder5{5};
builder5.build4().set_value(-1);// not moved here
Foofoo5=builder5.build4();
return0;
}
个人在尝试了4种情况后,认为第一种即返回值是Foo,内部用std::move的方式最好。
build2结果是引用,除了赋值时会copy之外,还存在可以通过build2修改内部foo_的问题。
build3纯粹是copy。
build4在赋值时move,但是存在通过build4修改内部foo_的问题。
最后build在赋值时move,不存在通过build修改内部foo_的问题。
总结
C++11引入的move语义带来很多变化,个人认为理解move语义对于写好C++11的代码很重要。希望我的分析对各位有用。