std::vector::push_back() 不会在 MSVC 上为具有已删除移动构造函数的对象进行编译
std::vector::push_back() doesn't compile on MSVC for an object with deleted move constructor
我有一个带有已删除移动构造函数的 class,当我尝试在 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; }
一切都编译和 运行s,输出如下:
ctor-dflt
ctor-copy
dtor
dtor
符合预期。没有调用移动构造函数。 (直播代码:https://rextester.com/XWWA51341)
此外,这两个版本在 gcc 上都运行良好。 (直播代码:https://rextester.com/FMQERO10656)
所以我的问题是,为什么在 MSVC 中对不可移动对象的 std::vector::push_back() 调用不编译,即使显然从未调用过移动构造函数?
std::vector<T>::push_back()
需要T
来满足MoveInsertable概念(实际上涉及分配器Alloc
)。这是因为向量上的 push_back
可能需要增长向量,移动(或复制)其中已有的所有元素。
如果您将 T
的 move c'tor 声明为已删除,那么,至少对于默认分配器 (std::allocator<T>
),T
不再是 移动插入。请注意,这与未声明移动构造函数的情况不同,例如因为只能隐式生成一个复制c'tor,或者因为只声明了一个复制c'tor,在这种情况下类型仍然是MoveInsertable,但复制c'tor是实际上调用(这有点违反直觉TBH)。
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),除了与标准库开发人员交谈或查看源代码外,没有其他方法可以知道……我的快速猜测是,如果您关心的话,根本没有必要实施此检查about 正在使法律程序发挥作用。根据标准,在 push_back
(或 reserve
等)期间重定位现有元素可以通过以下三种方式中的任何一种进行:
- 如果
std::is_nothrow_move_constructible_v<T>
,则元素不被移动(并且该操作是强异常安全的)。
- 否则,如果
T
是 CopyInsertable,则复制元素(并且该操作是强异常安全的)。
- 否则,将移动元素(并且未指定移动 c'tor 中引发的异常的影响)。
由于你的类型不是nothrow move c'tible,而是有一个copy c'tor,所以可以选择第二个选项。这是宽松的,因为没有对 MoveInsertable 进行检查。这可能是实施疏忽,或者我可能被故意忽略。 (如果类型不是 MoveInsertable,则整个调用格式错误,因此缺少检查不会影响格式正确的程序。)
至于 (B),接受代码的 IMO 实现违反了标准,因为它们不发出诊断信息。这是因为 要求 对格式不正确的程序(这包括使用该实现提供的语言扩展的程序)发出诊断,除非标准中另有说明,两者都不是这里就是这种情况。
这是未定义的行为,所以 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:
Requires: T shall be CopyInsertable into x
我们可以看到 CopyInsertable 需要 MoveInsertable [container.requirements#general]p15:
T is CopyInsertable into X means that, in addition to T being MoveInsertable into X...
在这种情况下 A
不是 MoveInsertable。
我们可以通过查看 [res.on.required]p1:
看到这是未定义的行为
Violation of the preconditions specified in a function's Requires: paragraph results in undefined behavior unless the function's Throws: paragraph specifies throwing an exception when the precondition is violated.
[res.on.required] 属于 Library-wide requirements.
在这种情况下,我们没有抛出段落,因此我们有未定义的行为,不需要诊断,正如我们从其 definition:
中看到的那样
behavior for which this International Standard imposes no requirements....
请注意,这与需要诊断的格式错误有很大不同,我在 my answer here 中解释了所有细节。
我有一个带有已删除移动构造函数的 class,当我尝试在 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; }
一切都编译和 运行s,输出如下:
ctor-dflt
ctor-copy
dtor
dtor
符合预期。没有调用移动构造函数。 (直播代码:https://rextester.com/XWWA51341)
此外,这两个版本在 gcc 上都运行良好。 (直播代码:https://rextester.com/FMQERO10656)
所以我的问题是,为什么在 MSVC 中对不可移动对象的 std::vector::push_back() 调用不编译,即使显然从未调用过移动构造函数?
std::vector<T>::push_back()
需要T
来满足MoveInsertable概念(实际上涉及分配器Alloc
)。这是因为向量上的 push_back
可能需要增长向量,移动(或复制)其中已有的所有元素。
如果您将 T
的 move c'tor 声明为已删除,那么,至少对于默认分配器 (std::allocator<T>
),T
不再是 移动插入。请注意,这与未声明移动构造函数的情况不同,例如因为只能隐式生成一个复制c'tor,或者因为只声明了一个复制c'tor,在这种情况下类型仍然是MoveInsertable,但复制c'tor是实际上调用(这有点违反直觉TBH)。
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),除了与标准库开发人员交谈或查看源代码外,没有其他方法可以知道……我的快速猜测是,如果您关心的话,根本没有必要实施此检查about 正在使法律程序发挥作用。根据标准,在 push_back
(或 reserve
等)期间重定位现有元素可以通过以下三种方式中的任何一种进行:
- 如果
std::is_nothrow_move_constructible_v<T>
,则元素不被移动(并且该操作是强异常安全的)。 - 否则,如果
T
是 CopyInsertable,则复制元素(并且该操作是强异常安全的)。 - 否则,将移动元素(并且未指定移动 c'tor 中引发的异常的影响)。
由于你的类型不是nothrow move c'tible,而是有一个copy c'tor,所以可以选择第二个选项。这是宽松的,因为没有对 MoveInsertable 进行检查。这可能是实施疏忽,或者我可能被故意忽略。 (如果类型不是 MoveInsertable,则整个调用格式错误,因此缺少检查不会影响格式正确的程序。)
至于 (B),接受代码的 IMO 实现违反了标准,因为它们不发出诊断信息。这是因为 要求 对格式不正确的程序(这包括使用该实现提供的语言扩展的程序)发出诊断,除非标准中另有说明,两者都不是这里就是这种情况。
这是未定义的行为,所以 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:
Requires: T shall be CopyInsertable into x
我们可以看到 CopyInsertable 需要 MoveInsertable [container.requirements#general]p15:
T is CopyInsertable into X means that, in addition to T being MoveInsertable into X...
在这种情况下 A
不是 MoveInsertable。
我们可以通过查看 [res.on.required]p1:
看到这是未定义的行为Violation of the preconditions specified in a function's Requires: paragraph results in undefined behavior unless the function's Throws: paragraph specifies throwing an exception when the precondition is violated.
[res.on.required] 属于 Library-wide requirements.
在这种情况下,我们没有抛出段落,因此我们有未定义的行为,不需要诊断,正如我们从其 definition:
中看到的那样behavior for which this International Standard imposes no requirements....
请注意,这与需要诊断的格式错误有很大不同,我在 my answer here 中解释了所有细节。