std :: vector :: push_back()不能在MSVC上针对具有删除的move构造函数的对象进行编译

时间:2018-10-23 09:29:32

标签: c++ gcc visual-c++ push-back deleted-functions

我有一个带有删除的move构造函数的类,当我尝试在MSVC(v.15.8.7 Visual C ++ 2017)中调用std :: vector :: push_back()时,收到一条错误消息,提示我正在尝试访问删除的move构造函数。但是,如果我定义了move构造函数,则代码会编译,但永远不会调用move构造函数。这两个版本均可在gcc(v。5.4)上按预期方式编译和运行。

这是一个简化的示例:

DATE <- str_c(DATE,"-01") df$date <- as.Date(DATE,format="%Y-%m-%d")

在Visual Studio上编译时会出现以下错误:

#include <iostream>
#include <vector>

struct A
{
public:
    A() { std::cout << "ctor-dflt" << std::endl; }
    A(const A&) { std::cout << "ctor-copy" << std::endl; }
    A& operator=(const A&) { std::cout << "asgn-copy" << std::endl; return *this; }
    A(A&&) = delete;
    A& operator=(A&& other) = delete;
    ~A() { std::cout << "dtor" << std::endl; }
};


int main()
{
    std::vector<A> v{};
    A a;
    v.push_back(a);
}

但是,如果我定义了move构造函数而不是将其删除

error C2280: 'A::A(A &&)': attempting to reference a deleted function  

所有内容都会编译并运行,并显示以下输出:

 A(A&&) { std::cout << "ctor-move" << std::endl; }

符合预期。没有调用move构造函数。 (实时代码:https://rextester.com/XWWA51341

此外,两个版本在gcc上都能完美运行。 (实时代码:https://rextester.com/FMQERO10656

所以我的问题是,为何即使显然从未调用过移动构造函数,在MSVC中为何也不能在不可移动对象上编译对std :: vector :: push_back()的调用?

2 个答案:

答案 0 :(得分:4)

这是未定义的行为,因此gcc和MSVC都是正确的。

我最近在推特上发了一个类似的案例using std::vector::emplace_back with a type that has a deleted move constructor,就像这样,这是未定义的行为。因此,这里所有的编译器都是正确的,未定义的行为不需要诊断,尽管实现可以随意执行。

我们可以从[container.requirements.general] Table 88开始来了解其原因,它告诉我们push_back要求T CopyInsertable

  

要求:T必须可复制到x

,我们可以看到 CopyInsertable 需要 MoveInsertable [container.requirements#general]p15

  

T被CopyInsertable转换成X意味着,除了T被MoveInsertable转换成X ...

在这种情况下,A不是 MoveInsertable

我们可以通过在[res.on.required]p1处查看这是未定义的行为:

  

违反函数的Requires:段落中指定的前提条件会导致未定义的行为,除非该函数的Throws:段落指定在违反前提条件时抛出异常。

[需要。] 位于Library-wide requirements下。

在这种情况下,我们没有throws段落,所以我们有未定义的行为,这不需要诊断,正如我们从其definition可以看到的那样:

  

本国际标准没有规定的行为。...

请注意,这与需要诊断的格式错误非常不同,我在my answer here中解释了所有详细信息。

答案 1 :(得分:3)

std::vector<T>::push_back()要求T满足 MoveInsertable 概念(实际上涉及分配器Alloc)。这是因为矢量上的push_back可能需要增大矢量,从而移动(或复制)其中已有的所有元素。

如果您声明T的移动符号已删除,那么至少对于默认分配器(std::allocator<T>),T不再是 MoveInsertable 。请注意,这与未声明move构造函数的情况不同,例如因为只能隐式生成一个副本c'tor,或者因为只声明了一个副本c'tor,在这种情况下,该类型仍为 MoveInsertable ,但实际上该副本c'tor被称为(这有点违反直觉。)

之所以从未实际调用move c'tor的原因是,您仅插入了一个元素,因此在运行时不需要移动现有元素。重要的是,您对push_back的论据本身是 lvalue ,因此在任何情况下都将被复制且不会移动。

更新:由于评论中的反馈,我不得不仔细研究一下。拒绝该代码的MSVC版本实际上是正确的(显然,2015年和2018年前期都这样做,但是2017年接受了该代码)。由于您正在呼叫push_back(const T&),因此T必须为 CopyInsertable 。但是, CopyInsertable 被定义为a strict subset of MoveInsertable。由于您的类型不是 MoveInsertable ,所以根据定义它也不是 CopyInsertable (请注意,如上所述,即使类型只能复制,类型也可能满足这两个概念,只要未明确删除移动c'tor即可。

这当然会引起更多问题:(A)为什么GCC,Clang和某些版本的MSVC仍然接受该代码,并且(B)他们这样做是否违反了标准?

对于(A),除了与标准库开发人员交谈或查看源代码外,没有其他方法可以知道。我的快速猜测是,如果您关心的只是实现此检查,则完全没有必要使法律程序有效。根据标准,在push_back(或reserve等期间)中现有元素的重定位可以通过以下三种方式之一进行:

  • 如果为std::is_nothrow_move_constructible_v<T>,则元素不会向左移动(并且该操作强烈地例外安全)。
  • 否则,如果T CopyInsertable ,则将复制元素(并且该操作强烈安全地例外)。
  • 否则,元素将移动(并且未指定移动c'tor中引发的异常的影响)。

由于您的文字不是非可移动的,而是具有副本的,因此可以选择第二个选项。这很宽容,因为没有检查 MoveInsertable 。这可能是实施监督,或者我可能被故意忽略。 (如果类型不是 MoveInsertable ,则整个调用格式不正确,因此,此丢失的检查不会影响格式正确的程序。)

对于(B),IMO接受代码的实现违反了该标准,因为它们没有发出诊断信息。这是因为除非标准中另有说明,否则需要实施以发出格式错误的程序的诊断信息(包括使用该实现提供的语言扩展的程序)。在这里。