C++ 容器内元素重复析构的问题
说明
std::vector 这种连续空间的容器,当空间不足时需要整体重新分配内存,并将旧的数据迁移过去。
首先会使用 std::move_if_noexcept 尝试进行移动。
因此如果元素类型的移动构造函数没有标明 noexcept 则不会被调用。
之后会通过 std::uninitialized_copy 尝试进行拷贝。
这是因为移动中如果产生异常,部分源数据已经被移动,将无法恢复原状。而拷贝中如果发生异常,源数据不应改变,只要返回失败即可。
参考:
示例
示例1 - 普通的类 :
1#include <cstdio>
2#include <vector>
3
4struct Demo
5{
6 ~Demo() {printf("destructor\n");}
7 Demo() {printf("constructor\n");}
8 Demo(const Demo&) {printf("copy\n");}
9 Demo(Demo&&) {printf("move\n");} // 没有标注 noexcept,调用拷贝
10};
11
12
13int main()
14{
15 std::vector<Demo> vec;
16 for (size_t i = 0; i < 2; i++)
17 {
18 vec.emplace_back();
19 }
20
21 return 0;
22}示例2 - 普通的模板类 :
1#include <cstdio>
2#include <vector>
3
4template<size_t N>
5struct Demo
6{
7 ~Demo() {printf("destructor\n");}
8 Demo() {printf("constructor\n");}
9 Demo(const Demo<N>&) {printf("copy\n");}
10 Demo(Demo<N>&&) {printf("move\n");} // 没有标注 noexcept,调用拷贝
11
12 char data[N];
13};
14
15
16int main()
17{
18 std::vector<Demo<64>> vec;
19 for (size_t i = 0; i < 2; i++)
20 {
21 vec.emplace_back();
22 }
23
24 return 0;
25}示例3 - 构造函数为模板的类 :
1#include <cstdio>
2#include <vector>
3
4template<size_t N>
5struct Demo
6{
7 ~Demo() {printf("destructor\n");}
8 Demo() {printf("constructor\n");}
9
10 // Demo<U> 和 Demo<N> 不是同一个类型,因此这不是移动构造函数
11 // 但是如果标注 noexcept,由于没有 默认移动构造函数 Demo(const Demo<N>&&)
12 // std::move_if_noexcept 会通过普通的右值引用参数匹配调用此函数
13 template<size_t U>
14 explicit Demo(Demo<U>&&) {printf("move 1\n");}
15
16 // Demo<U> 和 Demo<N> 不是同一个类型,因此这不是拷贝构造函数
17 // 由于有默认移动构造函数 Demo(const Demo<N>&)
18 // std::uninitialized_copy 会调用默认拷贝构造函数,而不会调用此函数
19 template<size_t U>
20 Demo(const Demo<U>&) {printf("copy 1\n");}
21
22 // 没有显式的拷贝和移动构造函数
23 // 会自动生成默认的拷贝构造函数 Demo(const Demo<N>&);
24 // std::uninitialized_copy 会调用 它进行浅拷贝
25 // 如果管理指针,析构函数会产生二次释放
26
27
28 // 如果删除默认拷贝构造函数:Demo(const Demo<N>&) = delete;
29 // template<size_t U> Demo(const Demo<U>&); 则也不会产生该实例
30 // std::uninitialized_copy 将无法在 U == T 时调用,只能在不同时调用
31
32 char data[N];
33};
34
35
36int main()
37{
38 std::vector<Demo<64>> vec;
39 for (size_t i = 0; i < 2; i++)
40 {
41 vec.emplace_back();
42 }
43
44 return 0;
45}解决方案
当需要实现类似上述 示例3 的场景(存在类似拷贝构造函数或移动构造函数的模板函数)时,应当对真正的拷贝构造函数和真正的移动构造函数进行显式特例化。
1#include <cstdio>
2#include <vector>
3
4template<size_t N>
5struct Demo
6{
7 ~Demo()
8 {
9 printf("destructor %p\n", this );
10 delete[] data;
11 }
12 Demo()
13 {
14 printf("constructor %p\n", this);
15 data = new float[N];
16 }
17
18 // Demo<U> 和 Demo<N> 不是同一个类型,因此这不是真正的移动构造函数
19 template<size_t U>
20 explicit Demo(Demo<U>&& src) noexcept
21 {
22 printf("fake move %p\n", this);
23
24 if (U >= N)
25 {
26 data = src.data;
27 src.data = nullptr;
28 }
29 else
30 {
31 data = new float[N]{0};
32 std::copy(data, data + U, src.data);
33 }
34 }
35
36 // Demo<U> 和 Demo<N> 不是同一个类型,因此这不是真正的拷贝构造函数
37 template<size_t U>
38 explicit Demo(const Demo<U>& src)
39 {
40 printf("fake copy %p\n", this);
41 data = new float[N]{0};
42 if (U >= N)
43 {
44 std::copy(data, data + N, src.data);
45 }
46 else
47 {
48 std::copy(data, data + U, src.data);
49 }
50 }
51
52 // 显式特化真正的移动构造函数
53 explicit Demo(Demo<N>&& src) noexcept
54 {
55 printf("true move %p\n", this);
56 data = src.data;
57 src.data = nullptr;
58 }
59
60 // 显式特化真正的拷贝构造函数
61 explicit Demo(const Demo<N>& src) noexcept
62 {
63 printf("true copy %p\n", this);
64 data = new float[N]{0};
65 std::copy(data, data + N, src.data);
66 }
67
68 float* data;
69};
70
71int main()
72{
73 // 相同类型调用真正的移动构造函数和真正的拷贝构造函数
74 {
75 Demo<64> demo1;
76 Demo<64> demo2{std::move_if_noexcept(demo1)};
77 Demo<64> demo3{demo2};
78 }
79
80 // 不同类型调用模板函数
81 {
82 Demo<64> demo1;
83 Demo<32> demo2{std::move_if_noexcept(demo1)};
84 Demo<128> demo3{demo2};
85 }
86 return 0;
87}1==16139== Memcheck, a memory error detector
2==16139== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
3==16139== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
4==16139== Command: ./a.out
5==16139==
6constructor 0x1ffefffbb0
7true move 0x1ffefffbb8
8true copy 0x1ffefffbc0
9destructor 0x1ffefffbc0
10destructor 0x1ffefffbb8
11destructor 0x1ffefffbb0
12constructor 0x1ffefffbb0
13fake move 0x1ffefffbb8
14fake copy 0x1ffefffbc0
15destructor 0x1ffefffbc0
16destructor 0x1ffefffbb8
17destructor 0x1ffefffbb0
18==16139==
19==16139== HEAP SUMMARY:
20==16139== in use at exit: 0 bytes in 0 blocks
21==16139== total heap usage: 6 allocs, 6 frees, 75,008 bytes allocated
22==16139==
23==16139== All heap blocks were freed -- no leaks are possible
24==16139==
25==16139== For lists of detected and suppressed errors, rerun with: -s
26==16139== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)