对于具有已删除移动构造函数的对象,std :: vector :: push_back()不会在MSVC上编译

Bre*_*ent 12 c++ gcc push-back visual-c++ deleted-functions

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

这是一个简化的例子:

#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);
}
Run Code Online (Sandbox Code Playgroud)

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

error C2280: 'A::A(A &&)': attempting to reference a deleted function  
Run Code Online (Sandbox Code Playgroud)

但是,如果我定义移动构造函数而不是删除它

 A(A&&) { std::cout << "ctor-move" << std::endl; }
Run Code Online (Sandbox Code Playgroud)

一切都编译并运行,输出如下:

ctor-dflt
ctor-copy
dtor
dtor
Run Code Online (Sandbox Code Playgroud)

正如所料.没有调用移动构造函数.(实时代码:https://rextester.com/XWWA51341)

而且,这两个版本在gcc上运行得非常好.(实时代码:https://rextester.com/FMQERO10656)

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

Sha*_*our 6

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

我最近在推特上发布了一个使用 std::vector::emplace_back的类似案例,该案例的类型具有已删除的移动构造函数,就像这个一样,它是未定义的行为。所以这里所有的编译器都是正确的,未定义的行为不需要诊断,尽管实现是自由的。

我们可以从[container.requirements.general] 表 88开始看到推理,它告诉我们push_backrequiresTCopyInsertable

要求:T 应该是 CopyInsertable 到 x

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

T 是 CopyInsertable 到 X 意味着,除了 T 是 MoveInsertable 到 X...

在这种情况下A不是MoveInsertable

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

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

[res.on.required]属于图书馆范围的要求

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

本国际标准未对其提出要求的行为......

请注意,这与需要诊断的格式错误非常不同,我在这里解释了我的答案中的所有细节。


Arn*_*gel 5

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

\n\n

如果将 的移动控制声明为已T删除,则至少对于默认分配器 ( std::allocator<T>) 来说,T不再是MoveInsertable。请注意,这与未声明移动构造函数的情况不同,例如,因为只能隐式生成复制对象,或者因为仅声明了复制对象,在这种情况下,类型仍然是MoveInsertable,但实际上调用了复制器(这有点违反直觉)。

\n\n

从未实际调用 move c\'tor 的原因是您只插入一个元素,因此在运行时不需要移动现有元素。重要的是,你对push_back自己的论证是一个左值,因此在任何情况下都会被复制并且不会移动。

\n\n

更新:我必须更仔细地研究这个(感谢评论中的反馈)。拒绝该代码的 MSVC 版本实际上这样做是正确的(显然,2015 年和 2018 年之前都这样做,但 2017 年接受该代码)。由于您正在调用push_back(const T&)T因此需要是CopyInsertable。但是,CopyInsertable被定义为MoveInsertable的严格子集。由于您的类型不是MoveInsertable,因此它不是CopyInsertable(请注意,如上所述,只要未显式删除移动 c\'tor,类型就可以满足这两个概念,即使它只是可复制的)。

\n\n

当然,这引发了更多问题:(A) 为什么 GCC、Clang 和 MSVC 的某些版本无论如何都接受这些代码,以及 (B) 他们这样做是否违反了标准?

\n\n

至于(A),除了与标准库开发人员交谈或查看源代码\xe2\x80\xa6之外,没有其他办法知道,我的快速猜测是,如果你关心的话,根本没有必要实现此检查重点是让法律程序发挥作用。在push_back(或reserve根据标准,

\n\n
    \n
  • 如果std::is_nothrow_move_constructible_v<T>,则元素不会被抛出移动(并且该操作是强异常安全的)。
  • \n
  • 否则,如果TCopyInsertable,则复制元素(并且该操作是强异常安全的)。
  • \n
  • 否则,元素将被移动(并且移动控制器中引发的异常的影响未指定)。
  • \n
\n\n

由于您的类型不是无投掷移动选项,而是有复制选项,因此可以选择第二个选项。这是宽松的,因为没有对MoveInsertable进行检查。这可能是实施疏忽,或者 i 可能被故意忽略。(如果类型不是MoveInsertable,则整个调用格式不正确,因此缺少检查不会影响格式良好的程序。)

\n\n

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

\n

  • [Clang](https://wandbox.org/permlink/wHBU0QjPXL3R3G48) 和 gcc 编译给定的代码。 (2认同)