move std 函数 示例_【C++11】move构造函数和std::move

本文详细介绍了C++11中的Move语义,包括move构造函数和std::move的使用,以及它们如何改变过去最佳实践。通过一个自定义类Foo的示例,阐述了数据所有权转移的概念,分析了如何在函数参数传递和返回值中应用Move语义。同时,讨论了在构建Wrapper类和Builder模式中如何正确使用Move语义,以提高代码效率和避免资源浪费。
摘要由CSDN通过智能技术生成

如果说新的语言特性使得过去的最佳实践不再成立的话,我想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的代码很重要。希望我的分析对各位有用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值