c++ 拷贝构造和移动构造同时存在 导致 move 的行为差异

首先我粗浅的认为同时存在没有问题,因为

  • 一个是创建一个变量副本
  • 一个转移一个生存周期将到的变量所申请的资源

环境

g++.exe (Rev2, Built by MSYS2 project) 9.2.0

g++.exe (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0

实验

sf 抽风,源码见 http://code.bulix.org/k07gcm-891664


A( const A& c )A( A&& c ) 同时存在的输出

this: 0x66f880 @ A()0
        申请内存:0x732760
this: 0x737d50 @ A( A&& c )0
        移动内存: 0x732760
this: 0x66f880 @ ~A()0
        被移动内存: 0

this: 0x66f880 @ A()1
        申请内存:0x737d70
this: 0x737dc0 @ A( A&& c )1
        移动内存: 0x737d70
this: 0x737db0 @ A( const A& c )99  // ???
        this: 0x737d50 0
        申请内存:0x737de0

this: 0x737d50 @ ~A()0
        释放内存: 0x732760
this: 0x66f880 @ ~A()1
        被移动内存: 0
over
0x737db0 : 99 // ???
0x737dc0 : 1
this: 0x737db0 @ ~A()99
        释放内存: 0x737de0
this: 0x737dc0 @ ~A()1
        释放内存: 0x737d70

可见 调用了拷贝构造函数,这是不是内部没有使用 std::move 导致调用拷贝构造函数?(瞎猜)

只有 A( A&& c ) 时的输出

this: 0x66f880 @ A()0
        申请内存:0x1f2760
this: 0x1f7d50 @ A( A&& c )0
        移动内存: 0x1f2760
this: 0x66f880 @ ~A()0
        被移动内存: 0

this: 0x66f880 @ A()1
        申请内存:0x1f7d70
this: 0x1f7dc0 @ A( A&& c )1
        移动内存: 0x1f7d70
this: 0x1f7db0 @ A( A&& c )0
        移动内存: 0x1f2760
this: 0x1f7d50 @ ~A()0
        被移动内存: 0
this: 0x66f880 @ ~A()1
        被移动内存: 0
over
0x1f7db0 : 0
0x1f7dc0 : 1
this: 0x1f7db0 @ ~A()0
        释放内存: 0x1f2760
this: 0x1f7dc0 @ ~A()1
        释放内存: 0x1f7d70

问题 2

class A a( 2 );
vect.push_back( std::move( a ) );

输出

this: 0x66f880 @ A()0
        申请内存:0x1f2760
this: 0x1f7d50 @ A( A&& c )0
        移动内存: 0x1f2760
this: 0x66f880 @ ~A()0
        被移动内存: 0

这个应该是 push_back 的源码

push_back(value_type&& __x)
{ emplace_back(std::move(__x)); }

为什么只调一次移动构造函数,被优化了?

+------------------------------+
|   在 这 里 创 建 变 量        |
+------------------------------+

vect.push_back( std::move( a ) );
+------------------------------+
|    调 用 移 动 构 造 函 数     |
+------------------------------+

emplace_back(std::move(__x));   //  <<<
+------------------------------+
| push_back 内 部 也 有 move    |
| 为 什 么 不 会 再 次 调 用 移 动 构 造    |
| 函 数                        |
+------------------------------+
阅读 4.9k
2 个回答

1) std::vector::push_back 有一个很强的异常安全保证,在元素类型可以拷贝构造的情况下,那么如果 push_bach 抛出任何异常,那么 std::vector 的原始状态不变。

vector.modifiers/1

...... If an exception is thrown while inserting a single element at the end and T is CopyInsertable or is_nothrow_move_constructible<T>::value is true, there are no effects. ......

push_back 可能引起内存分配(原来的内存不够了),这时原来的元素会拷贝/移动。如果使用移动构造来移动这些元素,那么,万一移动构造抛出异常,源对象和新对象的状态都已经被破坏了,无法保证异常安全性。所以有拷贝的时候,除非可以确保移动构造不抛出异常,否则只会使用拷贝构造函数。

没有拷贝构造函数时,只能使用移动构造函数。除非可以确保移动构造函数不抛出异常,否则不提供异常安全保证。

可以在移动构造函数上加上 noexcept ,保证其不抛出异常,这样就会使用移动构造函数了。见 https://www.ideone.com/9pyHdc

    A( A&& c ) noexcept {  

2) 这个叫 copy elision。

class.copy/31

copy elision

我的另一个关于 copy elision 的回答

我一步一步分析一下你的输出,看完你就明白了:

调用 a(0) 的构造函数输出:

this: 0x66f880 @ A()0
        申请内存:0x732760

vect 内部调用 a(0) 的移动构造函数输出:
注意:std::move 仅仅是告诉编译器,这是个右值,不会进行其它操作,见:https://en.cppreference.com/w...

this: 0x737d50 @ A( A&& c )0
        移动内存: 0x732760

调用局部变量 a(0) 的析构函数输出:

this: 0x66f880 @ ~A()0
        被移动内存: 0

调用 a(1) 的构造函数输出:

this: 0x66f880 @ A()1
        申请内存:0x737d70

C++ 标准要求,vectorpush_back() 的函数为强异常安全,见:https://en.cppreference.com/w...
意思是,如果发生异常,比如,内存分配失败,调用 push_back()vect 的值,和调用 push_back()vect 的值相同

在我的环境中,此时调用 vect.capacity(); 大小为 1,说明,如果再调用 vect.push_back(),内部将重新分配内存,
重新分配内存时需要构造原 vect 的值,由于编译器不知道移动构造函数是否会抛出异常,而移动构造函数将修改原值,
如果抛出异常,可能会导致原 vect 的部分元素的值和调用 push_back() 前的状态不一致,所以默认使用拷贝构造函数,拷贝构造函数不会修改原值,
如果想要使用移动构造函数,需要使用 noexcept,告诉编译器不会抛出异常,见:https://en.cppreference.com/w...

vect 调用 a(1) 的移动构造函数输出:

this: 0x737dc0 @ A( A&& c )1
        移动内存: 0x737d70

vect 调用 a(0) 的拷贝构造函数输出:

this: 0x737db0 @ A( const A& c )99  // ???
        this: 0x737d50 0
        申请内存:0x737de0

vect 的析构函数调用(a(0))的析构函数输出:

this: 0x737d50 @ ~A()0
        释放内存: 0x732760

调用局部变量 a(1) 的析构函数输出:

this: 0x66f880 @ ~A()1
        被移动内存: 0

输出 a(0)i 的值,由于拷贝构造函数并未初始化 i,所以 i 使用默认值

0x737db0 : 99 // ???

输出 a(1)i 的值

0x737dc0 : 1

vect 的析构函数调用(a(0))的析构函数输出:

this: 0x737db0 @ ~A()99
        释放内存: 0x737de0

vect 的析构函数调用(a(1))的析构函数输出:

this: 0x737dc0 @ ~A()1
        释放内存: 0x737d70

对于只有移动构造函数没有拷贝构造函数的情况,C++11 有特别规定

If T's move constructor is not noexcept and T is not CopyInsertable into *this, vector will use the throwing move constructor. If it throws, the guarantee is waived and
the effects are unspecified.

见:https://en.cppreference.com/w...

如果用户定义了移动构造函数或移动赋值运算符,编译器不会自动合成默认构造函数。

T has a user-defined move constructor or move assignment operator (this condition only causes the implicitly-declared, not the defaulted, copy constructor to be deleted).

见:https://en.cppreference.com/w...

顺便说一句,你的复制构造函数和复制构造运算符并没有赋值 i

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题