unordered_map的存在确定使用复制构造函数还是移动构造函数

时间:2019-03-09 01:47:55

标签: c++ constructor move move-semantics

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

这是一个最小的示例,其中类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));
    }   
}

如上所述,这是代码的工作状态:将元素添加到向量中并且必须将其重新分配为内存后,将调用默认的move构造函数,而不是copy构造函数,这一事实证明了“复制对象永远不会打印#,并且唯一指针字段仍然有效。

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

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

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

您可以通过取消注释任一 SUBFOO_MOVEFOO_MOVE_NONDEFAULT宏。

但是,尽管我有一些粗略的猜测(请参阅后记),但我大多感到困惑,并不真正理解为什么代码一开始就被破坏了,或者为什么没有一个修复程序可以解决它。有人可以很好地解释这里发生的事情吗?

P.S。我想知道的一件事是,尽管我可能会偏离轨道,但是如果SubFoo中无序映射的存在使Foo的移动构造不可行,那么编译器为什么不警告{{ 1}}移动构造函数是不可能的?

P.P.S。另外,尽管在这里显示的代码中,我尽可能使用了“ noexcept”移动构造函数,但对于是否可行,我还是有一些编译器分歧。例如,clang警告我,对于= default,“错误:显式默认move构造函数的异常说明与计算出的异常不匹配”。这与以上有关吗?也许在向量大小调整中使用的move构造函数一定是noexcept,而且我的某种方式并不是真的...

有关除此以外的修改 这里可能存在一些编译器依赖性,但是对于coliru使用的g ++版本,Foo(Foo&& rhs) noexcept = default的(默认)移动构造函数不需要不需要指定noexcept来修复矢量调整大小问题(与指定SubFoo不同,这是行不通的):

non-noexcept SubFoo move ctor works

noexcept(false)的自定义移动构造函数必须只能解决以下问题:

non-noexcept Foo move ctor does not work

1 个答案:

答案 0 :(得分:1)

(在我看来)有一个标准缺陷,即无序地图的移动ctor也不例外。

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

向量大小调整需要一个noexecept(true)移动ctor,因为它无法理智而有效地从第372个元素的移动抛出中恢复;它既不能回滚也不能继续前进。它必须以一些丢失的元素停止。