【问题标题】:Does Visual Studio 2017 need an explicit move constructor declaration?Visual Studio 2017 是否需要显式移动构造函数声明?
【发布时间】:2019-04-09 15:57:51
【问题描述】:

使用 Visual Studio 2015 可以成功编译以下代码,但使用 Visual Studio 2017 编译失败。Visual Studio 2017 报告:

错误 C2280:“std::pair::pair(const std::pair &)”:试图引用已删除的函数

代码

#include <unordered_map>
#include <memory>

struct Node
{
  std::unordered_map<int, std::unique_ptr<int>> map_;
  // Uncommenting the following two lines will pass Visual Studio 2017 compilation
  //Node(Node&& o) = default;
  //Node() = default;
};

int main()
{
  std::vector<Node> vec;
  Node node;
  vec.push_back(std::move(node));
  return 0;
}

看起来 Visual Studio 2017 显式需要移动构造函数声明。是什么原因?

【问题讨论】:

  • 您缺少#include &lt;vector&gt;,但据我所知,该代码应该可以编译(并且它可以在例如 GCC 8.2 上编译)。你有最新最好的VS2017吗?
  • 我可以确认此代码失败,错误代码在 vs2017 15.4.2 下。
  • @rubenvb 将在内部包含
  • @DanielLangr、@Oliv、@Evg 都可以。查看 Vs2017 的矢量源代码,vec.push_backvec.reserve 都调用 _Umove_if_noexcept,内部调用 _Uninitialized_copydisjunction_v&lt;is_nothrow_move_constructible&lt;_Ty&gt;, negation&lt;is_copy_constructible&lt;_Ty&gt;&gt;&gt; 决定
  • @finn 完全是实现定义的,实际上对于例如GCC 的 libstdc++。

标签: c++ visual-studio-2017 move-constructor


【解决方案1】:

小例子:

#include <memory>
#include <unordered_map>
#include <vector>

int main() {
  std::vector<std::unordered_map<int, std::unique_ptr<int>>> vec;
  vec.reserve(1);
}

GodBolt 上的现场演示:https://godbolt.org/z/VApPkH


另一个例子:

std::unordered_map<int, std::unique_ptr<int>> m;
auto m2 = std::move(m);              // ok
auto m3 = std::move_if_noexcept(m);  // error C2280

更新

我相信编译错误是合法的。 Vector 的重新分配函数可以使用std::move_if_noexcept 传递元素(的内容),因此更喜欢复制构造函数而不是抛出移动构造函数。

在 libstdc++ (GCC) / libc++ (clang) 中,std::unordered_map 的移动构造函数是(看似)noexcept。因此,Node 的移动构造函数也是noexcept,完全不涉及其复制构造函数。

另一方面,MSVC 2017 的实现似乎没有将 std::unordered_map 的移动构造函数指定为 noexcept。所以Node的move构造函数也不是noexcept,vector通过std::move_if_noexcept的重新分配函数试图调用Node的copy构造函数。

Node 的复制构造函数是隐式定义的,因此它会调用std::unordered_map 的复制构造函数。但是,此处可能不会调用后者,因为 map 的值类型(本例中为 std::pair&lt;const int, std::unique_ptr&lt;int&gt;&gt;)是不可复制的。

最后,如果你定义Node的移动构造函数,它的隐式声明的复制构造函数被定义为删除。 并且,IIRC,已删除隐式声明的复制构造函数不参与重载决议。但是,std::move_if_noexcept 不考虑删除的复制构造函数,因此它将使用Node. 的抛出移动构造函数

【讨论】:

  • 这真的是一个错误吗? std::is_copy_constructible_v&lt;std::unordered_map&lt;int, std::unique_ptr&lt;int&gt;&gt;&gt; 为真,即使任何复制此类类型的尝试都会导致编译错误。此外,它也会被检测为 CopyInsertable,即使任何尝试实例化复制插入的代码也会导致编译错误。
  • @Oliv 这是个好问题。似乎问题在于 std::unordered_map 的移动构造函数在 MSVC 2017 中不是 noexcept (标准似乎不需要,或者是吗?)。在 libstdc++/libc++ 中为noexcept
  • @Oliv 我更新了我的答案,我想你是对的。现在我什至不认为这很“令人惊讶”。
  • @DanielLangr 我在 CppLang 闲暇时问过。原来加强noexcept是合规的。
【解决方案2】:

当您声明移动构造函数时,隐式声明的复制构造函数被定义为已删除。另一方面,当您不声明移动构造函数时,编译器会在需要时隐式定义复制构造函数。而且这个隐含的定义格式不正确。

unique_ptr 在使用标准分配器的容器中不是 CopyInsertable,因为它不可复制构造,因此 map_ 的复制构造函数格式不正确(它可以被声明为已删除,但这不是标准要求)。

正如您的示例代码向我们展示的那样,对于较新版本的 MSVC,此格式错误的定义是使用此示例代码生成的。我认为标准中没有禁止它的内容(即使这确实令人惊讶)。

所以你确实应该确保 Node 的复制构造函数被声明或隐式定义为删除。

【讨论】:

  • 我想知道为什么在 clang 或 gcc 中看不到这个问题?
  • @darune 我认为该错误在标准要求中。如果标准类型不是可复制构造的,则应该可以使用特征对其进行检查。但实际实现这样的要求将是一个真正的痛苦。我希望通过概念来解决这个问题。
  • “正确”执行此操作的类型特征魔法非常复杂。我实现了一个开放寻址哈希映射(和设置),它主要与unordered_map 兼容,最终不得不使用来自std::conditional&lt;std::is_copy_constructible&lt;...&gt;::value &amp;&amp; ..., AllowCopy, DisallowCopy&gt;::type 的私有继承,其中AllowCopy 为空,DisallowCopy 已删除复制操作(其中其他事情)。我还检查了默认的 c'tible 分配器,所有这些仍然不太正确,例如我既不支持复制时的分配器传播也不支持复制分配器中的元素。
  • is_nothrow_move_constructible_v&lt;Node&gt; 在 MSVC 中是 false,所以它尝试复制并失败;它是 gcc 中的true,所以它会移动。如果在 gcc 中is_nothrow_move_constructible_v&lt;Node&gt; 被强制变为false,它也会尝试复制并失败。为什么 MSVC 和 gcc 在 is_nothrow_move_constructible_v&lt;Node&gt; 值上存在分歧?
  • @Evg 和is_copy_constructible&lt;Node&gt; 是真的,即使无法复制它!这是真的,因为is_copy_constructible_v&lt;unordered_map&lt;int,std::unique_ptr...&gt;&gt; 是真的,而标准应该要求它是假的。那么如果你检查 is_nothrow_copy_constructible 会发生什么只是混乱。
【解决方案3】:

让我们看一下std::vector源代码(我将pointer_Ty替换为实际类型):

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, true_type)
    {   // move [First, Last) to raw Dest, using allocator
    _Uninitialized_move(First, Last, Dest, this->_Getal());
    }

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, false_type)
{   // copy [First, Last) to raw Dest, using allocator
    _Uninitialized_copy(First, Last, Dest, this->_Getal());
}

void _Umove_if_noexcept(Node* First, Node* Last, Node* Dest)
{   // move_if_noexcept [First, Last) to raw Dest, using allocator
    _Umove_if_noexcept1(First, Last, Dest,
        bool_constant<disjunction_v<is_nothrow_move_constructible<Node>, negation<is_copy_constructible<Node>>>>{});
}

如果Node无抛移动构造不可复制构造,则调用_Uninitialized_move,否则调用_Uninitialized_copy

问题在于,如果您没有显式声明移动构造函数,Node 的类型特征 std::is_copy_constructible_vtrue。此声明使复制构造函数被删除。

libstdc++ 以类似的方式实现std::vector,但std::is_nothrow_move_constructible_v&lt;Node&gt;true,而MSVC 是false。因此,使用了移动语义,编译器不会尝试生成复制构造函数。

但是如果我们强制 is_nothrow_move_constructible_v 变成 false

struct Base {
    Base() = default;
    Base(const Base&) = default;
    Base(Base&&) noexcept(false) { }
};

struct Node : Base {
    std::unordered_map<int, std::unique_ptr<int>> map;
};

int main() {
    std::vector<Node> vec;
    vec.reserve(1);
}

同样的错误发生:

/usr/include/c++/7/ext/new_allocator.h:136:4: error: use of deleted function ‘std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2>&) [with _T1 = const int; _T2 = std::unique_ptr<int>]’
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

【讨论】:

  • 我想知道为什么在 clang 或 gcc 中没有看到这个问题?
  • @darune,愚蠢的答案是因为 gcc 和 clang 的实现在这段特定的代码中不依赖 std::is_copy_constructible。我现在没有更好的答案。
  • 如果我理解正确,代码在 MSVC 向量实现中(?),在这种情况下这不是一个错误吗?感觉确实像个 bug,因为直接调用 move 构造函数确实编译得很好。
  • @darune,是的,我的答案中的代码直接来自 MSVC STL 实现。我同意这感觉像是一个错误。
  • @darune,在 libstdc++ 中添加了一些信息。
【解决方案4】:

Visual Studio 2017:

正如@Evg 所指出的,Visual Studio 2017 的矢量源代码最终调用了 _Uninitialized_copy,因为在 Visual Studio 2017 中,隐式声明的 Node 移动构造函数被认为是 not-nothrow(is_nothrow_move_constructible&lt;Node&gt; 为假),is_copy_constructible&lt;Node&gt; 为真.

1) 关于is_nothrow_move_constructible&lt;Node&gt;

https://en.cppreference.com/w/cpp/language/move_constructor 说:

隐式声明(或在其第一个声明时默认)移动构造函数具有如dynamic exception specification(C++17 前)exception specification(C++17 起)中所述的异常规范

也许将is_nothrow_move_constructible&lt;Node&gt; 视为错误是合理的,因为Node 的数据成员std::unordered_map 的移动构造函数未标记为noexcept。

2) 关于is_copy_constructible&lt;Node&gt;

正如@Oliv 所说,将is_copy_constructible&lt;Node&gt; 计算为true 似乎不合逻辑,特别是考虑到Node 不是copy_constructible 已被Visual Studio 2017 编译器检测并报告为编译错误这一事实。 Node 不是 copy_constructible 因为std::unique_ptr 不是 copy_constructible。

Visual Studio 2015:

Visual Studio 2015 的向量有不同的实现。 vec.push_back->_Reserve->_Reallocate->_Umove->_Uninitialized_move_al_unchecked->_Uninitialized_move_al_unchecked1->std::move(node)is_nothrow_move_constructible&lt;Node&gt;is_copy_constructible&lt;Node&gt; 不涉及。它只是调用 std::move(node) 而不是复制构造函数。因此示例代码可以使用 Visual Studio 2015 成功编译。

【讨论】:

  • 这里的重要结论是恕我直言,MSVC 2017 符合标准。 std::unordered_map 的移动构造函数不是 noexcept,这很好。它的复制构造函数已定义(必须),即使它不能用于不可复制的值类型。这个实现没有任何问题。这就是 C++ 标准的编写方式。希望通过概念,我们可以更好地控制相应的内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-08-27
  • 1970-01-01
  • 2017-03-16
  • 1970-01-01
  • 1970-01-01
  • 2015-03-21
相关资源
最近更新 更多