UNORDERED_MAP的存在确定是否使用了复制构建器或移动构造器



在扩展一些预先存在的代码时,我遇到了涉及一些嵌套类的情况,并移动构造产生了非常出乎意料的行为。我最终能够制作两个可能的修复程序,但我不确定我完全理解要开始的问题。

这是一个最小的示例,其中Foo类包含类型SubFoo和独特指针的字段,并且具有不同的复制和移动构造器,以反映独特指针的所有权。请注意,有三个宏不确定 - 与代码的原始工作状态相对应(即没有断言失败)。

#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));
    }   
}

如上所述,这是代码的工作状态:由于元素被添加到向量中,并且必须重新分配内存,因此默认移动构造函数被称为而不是复制构造函数,这证明了"复制对象复制对象#"永远不会打印,唯一的指针字段仍然有效。

但是,在将无序的地图字段添加到SubFoo(在我的情况下,这不是完全空的,但仅包含更多的基本类型),在调整/重新定位向量时不再使用MOVE构造器。这是一个COLIRU链接,您可以在其中运行此代码,该代码具有启用ADDMAP宏,并导致断言失败,因为在向量调整大小时调用了复制构造函数,并且唯一的指针变得无效。

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

  • 添加SubFoo的默认移动构造函数
  • 使用Foo的非默认移动构造函数看起来完全像我想象的默认移动构造函数。

您可以在Coliru中尝试一下这些 SUBFOO_MOVEFOO_MOVE_NONDEFAULT宏。

但是,尽管我有一些粗糙的猜测(请参阅后记),但我主要感到困惑,并且并不真正理解为什么该代码首先被打破了,也不是为什么任何一个修复程序修复了它。有人可以很好地解释这里发生的事情吗?

P.S。我想知道的一件事,尽管我可能会偏离轨道,但如果SubFoo中无序地图的存在使Foo的移动构造不可接受,那么编译器为什么不警告= default移动构造器是不可能的?

P.P.S。此外,在此处显示的代码中,我使用了" noexcept"移动构造函数,但我对这是否可能存在一些编译器分歧。例如,Clang警告我,对于Foo(Foo&& rhs) noexcept = default,"错误:明确默认的移动构造函数的异常规范与计算的一个不匹配"。这与上述有关吗?也许在调整矢量调整大小中使用的移动构造函数一定是NoExcept,我的确不是真正的...

编辑noexcept 此处可能有一些编译器依赖性,但是对于Coliru使用的G 版本,SubFoo的(默认)移动构造函数di em> not 不需要指定未指定的except才能解决矢量调整问题(这是与指定noexcept(false)不起作用的内容不同):

非NoExcept子Foo移动CTOR Works

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

non-noexcept foo移动ctor不起作用

有一个标准缺陷(我认为)无序地图的移动ctor并非没有。

因此,默认的移动ctor为noexcept(false)或被您未曾尝试的默认noexcept(true)删除似乎是合理的。

矢量调整大小需要一个noecept(true)移动CTOR,因为它不能从第372个元素的抛出中恢复过来,有效地恢复;它既不能退缩,也不能继续前进。它必须以某种方式缺少一堆元素。

最新更新