c++ - unordered_map 的存在决定了是使用复制构造函数还是 move 构造函数

标签 c++ constructor move move-semantics

在扩展一些预先存在的代码时,我遇到了涉及一些嵌套类和 move 构造的情况,这些情况产生了非常意外的行为。我最终能够生成两个可能的修复程序,但我不确定我是否完全理解问题的开始。

这是一个稍微小一点的例子,其中一个类 Foo 包含一个类型为 SubFoo 的字段和一个唯一的指针,并且有不同的复制和 move 构造函数来反射(reflect)唯一指针的所有权。请注意,有三个未定义的宏 --- 对应于代码的原始工作状态(即没有断言失败)。

#include <iostream>
#include <unordered_map>
#include <memory>
#include <vector>
#include <cassert>

//#define ADDMAP
//#define SUBFOO_MOVE
//#define FOO_MOVE_NONDEFAULT

class SubFoo {
public:
    SubFoo() {}
    SubFoo(const SubFoo& rhs) = default;
#ifdef SUBFOO_MOVE
    SubFoo(SubFoo&& rhs) noexcept = default;
#endif
private:
#ifdef ADDMAP
    std::unordered_map<uint32_t,uint32_t> _map;
#endif
};

class Foo {
public:
    Foo(const std::string& name, uint32_t data)
    : _name(name),
      _data(std::make_unique<uint32_t>(std::move(data))),
      _sub()
    {       
    }

    Foo(const Foo& rhs)
    : _name(rhs._name),
      _data(nullptr),
      _sub(rhs._sub)
    {
        std::cout << "\tCopying object " << rhs._name << std::endl;
    }

#ifdef FOO_MOVE_NONDEFAULT
    Foo(Foo&& rhs) noexcept
     : _name(std::move(rhs._name)),
       _data(std::move(rhs._data)),
       _sub(std::move(rhs._sub))
    {
        std::cout << "\tMoving object " << rhs._name << std::endl;
    }
#else
    Foo(Foo&& rhs) noexcept = default;
#endif

    std::string _name;
    std::unique_ptr<uint32_t> _data;
    SubFoo _sub;
};

using namespace std;
int main(int,char**) {
    std::vector<Foo> vec;

    /* Add elements to vector so that it has to resize/reallocate */
    cout << "ADDING PHASE" << endl;
    for (uint i = 0; i < 10; ++i) {
        std::cout << "Adding object " << i << std::endl; 
        vec.emplace_back(std::to_string(i),i);
    }
    cout << endl;

    cout << "CHECKING DATA..." << endl;
    for (uint i = 0; i < vec.size(); ++i) {
        const Foo& f = vec[i];
        assert(!(f._data.get() == nullptr || *f._data != i));
    }   
}

如上所述,这是代码的工作状态:当元素被添加到 vector 中并且它必须重新分配内存时,默认的 move 构造函数被调用而不是复制构造函数,正如“Copying object #"永远不会被打印出来,唯一的指针字段仍然有效。

但是,在将无序映射字段添加到 SubFoo 之后(在我的例子中它不是完全空的,而是只包含更多基本类型),在调整大小/重新分配时不再使用 move 构造函数 vector 。 Here is a coliru link您可以在其中运行此代码,该代码启用了 ADDMAP 宏并导致断言失败,因为在 vector 调整大小期间调用了复制构造函数并且唯一指针变得无效。

我最终找到了两个解决方案:

  • SubFoo 添加默认 move 构造函数
  • Foo 使用非默认 move 构造函数,这看起来与我想象的默认 move 构造函数所做的完全一样。

您可以在 coliru 中通过取消注释任何一个来尝试这些 SUBFOO_MOVEFOO_MOVE_NONDEFAULT 宏。

然而,虽然我有一些粗略的猜测(见后记),但我大多感到困惑,并不真正理解为什么代码首先被破坏,也不明白为什么其中一个修复程序修复了它。有人可以很好地解释这里发生的事情吗?

附言我想知道的一件事是,尽管我可能偏离了轨道,如果 SubFoo 中无序映射的存在以某种方式使 Foo 的 move 构造不可行,为什么不编译器警告说 = default move 构造函数是不可能的?

附言此外,虽然在此处显示的代码中我尽可能使用了“noexcept” move 构造函数,但我对这是否可能存在一些编译器分歧。例如,clang 警告我对于 Foo(Foo&& rhs) noexcept = default,“错误:显式默认 move 构造函数的异常规范与计算的构造函数不匹配”。跟上面的有关系吗?也许在 vector 调整大小中使用的 move 构造函数必须是 noexcept,而我的并不是真的......

关于 NOEXCEPT 的编辑 这里可能存在一些编译器依赖性,但对于 coliru 使用的 g++ 版本,SubFoo 的(默认) move 构造函数确实需要指定 noexcept 才能修复 vector 调整大小问题(这与指定 noexcept(false) 不同,后者不起作用):

non-noexcept SubFoo move ctor works

虽然 Foo 的自定义 move 构造函数 必须 是 noexcept 以修复问题:

non-noexcept Foo move ctor does not work

最佳答案

有一个标准缺陷(在我看来),即无序映射的 move 构造函数不是 noexcept。

因此默认 move 构造函数是 noexcept(false) 或被您尝试的默认 noexcept(true) 删除似乎是合理的。

vector 调整大小需要一个 noexecept(true) move 构造函数,因为它不能从第 372 个元素的 move 抛出中清醒有效地恢复;它既不能回滚也不能继续前进。它必须以某种方式丢失一堆元素而停止。

关于c++ - unordered_map 的存在决定了是使用复制构造函数还是 move 构造函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55073216/

相关文章:

javascript - ES6 默认参数为空对象?

java - move 文件而不释放锁

c++ - vector::push_back 和 std::move

delphi - 将动态记录数组的变量复制到另一个变量,但它们共享一个地址

c++ - 为什么 0 mod 0 是一个错误?

c++ - codeblocks c++ 停止工作可能是由于引用

c++ - 继承的构造函数和附加变量的解决方法

c++ - 如何为 ns3 中的类继承 SimpleRefCount 子类(网络模拟器 3)

c++ - 为什么转换可变参数函数参数很重要?

python - 从构造函数中以正确的顺序调用加载函数