std::vector::p ush_back() 不会在 MSVC 上编译具有已删除移动构造函数的对象
std::vector::push_back() doesn't compile on MSVC for an object with deleted move constructor
我有一个带有已删除移动构造函数的类,当我尝试在MSVC(v.15.8.7 Visual C++2017)中调用std::vector::push_back()时,我收到一个错误,说我正在尝试访问已删除的移动构造函数。然而,如果我定义了移动构造函数,代码就会编译,但移动构造函数永远不会被调用。两个版本都按照预期在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);
}
在Visual Studio上编译时会出现以下错误:
error C2280: 'A::A(A &&)': attempting to reference a deleted function
然而,如果我定义移动构造函数而不是删除它
A(A&&) { std::cout << "ctor-move" << std::endl; }
所有内容都编译并运行,输出如下:
ctor-dflt
ctor-copy
dtor
dtor
正如预期的那样。没有对move构造函数的调用。(实时代码:https://rextester.com/XWWA51341)
此外,这两个版本在gcc上运行得非常好。(实时代码:https://rextester.com/FMQERO10656)
所以我的问题是,为什么对不可移动对象的std::vector::push_back()的调用不在MSVC中编译,即使移动构造函数显然从未被调用过?
这是未定义的行为,因此gcc和MSVC都是正确的。
我最近在推特上发布了一个类似的案例,使用std::vector::template_back和一个删除了move构造函数的类型,就像这个一样,它是未定义的行为。所以这里所有的编译器都是正确的,未定义的行为不需要诊断,尽管实现是免费的
我们可以从[container.requirements.general]表88开始看出原因,表88告诉我们push_back
要求T
为CopyInsertable:
要求:T应可复制插入x
并且我们可以看到CopyInsertable需要MoveInsertable[container.requirements#general]p15:
T是可复制插入X意味着,除了T是可移动插入X…
在这种情况下,A
不是可移动插入的。
我们可以通过在[res.on.required]p1:中搜索来看到这是未定义的行为
违反函数的Requires:paragraph中指定的先决条件会导致未定义的行为,除非函数的Throws:paragraph指定在违反先决条件时抛出异常。
[res.on.required]属于库范围的要求。
在这种情况下,我们没有throws段落,因此我们有未定义的行为,这不需要诊断,正如我们从其定义中看到的那样:
本国际标准未对其提出任何要求的行为。。。。
注意,这与需要诊断的形式错误非常不同,我在这里解释了我的答案中的所有细节。
std::vector<T>::push_back()
要求T
满足MoveInsertable概念(实际上涉及分配器Alloc
)。这是因为向量上的push_back
可能需要增长向量,移动(或复制)已经在其中的所有元素
如果您将T
的移动c'tor声明为已删除,那么,至少对于默认分配器(std::allocator<T>
),T
不再是MoveInsertable。请注意,这与没有声明移动构造函数的情况不同,例如,因为只能隐式生成副本c'tor,或者因为只声明了副本c'tor,在这种情况下,类型仍然是MoveInsertable,但实际上调用了副本c'tor(这有点违反直觉TBH)。
移动c'tor从未被实际调用的原因是,您只插入一个元素,因此在运行时不需要移动现有元素。重要的是,您对push_back
的自变量本身是左值,因此在任何情况下都会被复制而不移动。
更新:我不得不更仔细地研究这个问题(感谢评论中的反馈)。拒绝该代码的MSVC版本实际上是正确的(显然,2015年和2018年之前都这样做,但2017年接受了该代码)。由于您正在调用push_back(const T&)
,因此T
必须是CopyInsertable。但是,CopyInsertable被定义为MoveInsertable的严格子集。由于您的类型不是MoveInsertable,因此根据定义,它也不是CopyInsertable(请注意,如上所述,一个类型可以满足这两个概念,即使它只是可复制的,只要没有显式删除movec'tor)。
当然,这引发了更多的问题:(A)为什么GCC、Clang和一些版本的MSVC无论如何都接受代码,以及(B)他们这样做是否违反了标准?
至于(A),除了与标准库开发人员交谈或查看源代码之外,没有其他方法可以知道……我的快速猜测是,如果你只关心让合法程序工作,那么根本没有必要实现这种检查。根据标准,在push_back
(或reserve
等)期间,可以通过以下三种方式中的任何一种重新定位现有元素:
- 如果
std::is_nothrow_move_constructible_v<T>
,则元素被另一行移动(并且该操作是强异常安全的) - 否则,如果
T
是CopyInsertable,则会复制元素(并且该操作是强异常安全的) - 否则,元素将被移动(并且在移动c'或中引发的异常的影响是未指定的)
由于您的类型不是nothrow move c'table,而是具有复制c'tor,因此可以选择第二个选项。这是宽松的,因为没有对MoveInsertable进行检查。这可能是执行上的疏忽,也可能是故意忽略了我。(如果类型不是MoveInsertable,则整个调用的格式不正确,因此此缺失检查不会影响格式正确的程序。)
至于(B),IMO接受代码的实现违反了标准,因为它们不发出诊断。这是因为需要一个实现来发出格式错误程序的诊断(这包括使用该实现提供的语言扩展的程序),除非标准中另有说明,否则两者都不是。
- std::vector::p ush_back() 不会在 MSVC 上编译具有已删除移动构造函数的对象
- std::任何只用于移动的模板,其中副本ctor内的static_assert等于编译错误,但为什么
- 编译对移动应用的影响?
- 初始化不可移动对象数组:为什么这样的代码无法在 GCC 上编译?
- 程序编译,但当分解为函数时实际上不会移动电机
- 提升移动编译错误
- 为什么删除移动构造函数会导致编译错误
- 将函数从控制台应用程序移动到共享库项目似乎会带来不相关的编译错误
- Visual Studio 2017:_MM_LOAD_PS经常被编译为移动
- 在向量中使用派生的可移动但不可压缩的会导致编译错误
- libstdc++已弃用;移动到libc++[-Wdeprecated],但是更改会产生编译错误
- 如果将功能的非常简单的定义移动到.cpp,则编译时间的减少是多少
- 只需添加任何事情都不会导致编译错误(围绕std ::移动),为什么
- 使用 pimpl 移动类无法编译
- 在Visual Studio中,我不想提交我的Debug文件夹,但它中有dll文件,如果我移动它,项目将无法编译
- 将 Windows C++ 项目从 Qt4 移动到 5 会给出数百个看似无关的编译错误
- 将模板方法移动到派生中断编译
- 返回仅移动类型编译,即使复制构造函数不可用
- 将错误从链接时移动到编译时
- C++ 移动构造函数无法编译