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)