我有一个带有删除的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()的调用?
答案 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 ,则将复制元素(并且该操作强烈安全地例外)。由于您的文字不是非可移动的,而是具有副本的,因此可以选择第二个选项。这很宽容,因为没有检查 MoveInsertable 。这可能是实施监督,或者我可能被故意忽略。 (如果类型不是 MoveInsertable ,则整个调用格式不正确,因此,此丢失的检查不会影响格式正确的程序。)
对于(B),IMO接受代码的实现违反了该标准,因为它们没有发出诊断信息。这是因为除非标准中另有说明,否则需要实施以发出格式错误的程序的诊断信息(包括使用该实现提供的语言扩展的程序)。在这里。