在向量中使用没有副本且没有 noexcept 移动构造函数的对象。到底是什么坏了,我该如何确认?
Using an object without copy and without a noexcept move constructor in a vector. What actually breaks and how can I confirm it?
我已经检查了很多移动 constructor/vector/noexcept 线程,但我仍然不确定当事情应该出错时实际发生了什么。我无法按预期产生错误,所以要么我的小测试有误,要么我对问题的理解有误。
我正在使用 BufferTrio 对象的向量,它定义了一个 noexcept(false) 移动构造函数,并删除所有其他 constructor/assignment 运算符,这样就没有什么可以回退到:
BufferTrio(const BufferTrio&) = delete;
BufferTrio& operator=(const BufferTrio&) = delete;
BufferTrio& operator=(BufferTrio&& other) = delete;
BufferTrio(BufferTrio&& other) noexcept(false)
: vaoID(other.vaoID)
, vboID(other.vboID)
, eboID(other.eboID)
{
other.vaoID = 0;
other.vboID = 0;
other.eboID = 0;
}
事物编译和运行,但来自https://xinhuang.github.io/posts/2013-12-31-when-to-use-noexcept-and-when-to-not.html:
std::vector will use move when it needs to increase(or decrease) the capacity, as long as the move operation is noexcept.
或来自 优化的 C++:经过验证的提高性能的技术作者:Kurt Guntheroth:
If the move constructor and move assignment operator are not declared noexcept, std::vector uses the less efficient copy operations instead.
由于我删除了这些,我的理解是这里应该有问题。但是那个向量 运行ning 没问题。因此,我还创建了一个基本循环,将 push_backs 半百万次变成一个虚拟向量,然后将该向量与另一个单元素虚拟向量交换。像这样:
vector<BufferTrio> thing;
int n = 500000;
while (n--)
{
thing.push_back(BufferTrio());
}
vector<BufferTrio> thing2;
thing2.push_back(BufferTrio());
thing.swap(thing2);
cout << "Sizes are " << thing.size() << " and " << thing2.size() << endl;
cout << "Capacities are " << thing.capacity() << " and " << thing2.capacity() << endl;
输出:
Sizes are 1 and 500000
Capacities are 1 and 699913
仍然没有问题,所以:
我应该看到哪里出了问题,如果是,我该如何演示?
向量重新分配尝试提供异常保证,即如果在重新分配操作期间抛出异常,则尝试保留原始状态。共有三种情况:
元素类型为nothrow_move_constructible
: 重新分配可以移动不会导致异常的元素。这是有效的情况。
元素类型为CopyInsertable
:如果类型不为nothrow_move_constructible
,这足以提供强保证,尽管在重新分配时进行了复制。这是旧的 C++03 默认行为,是效率较低的回退。
元素类型既不是 CopyInsertable
也不是 nothrow_move_constructible
。只要它仍然是可移动构造的,就像在您的示例中一样,向量重新分配是可能的,但不提供任何异常保证(例如,如果移动构造抛出,您可能会丢失元素)。
说明这一点的规范性措辞分布在各种重新分配职能中。例如,[vector.modifiers]/push_back
表示:
If an exception is thrown while
inserting a single element at the end and T
is CopyInsertable
or is_nothrow_move_constructible_v<T>
is true
, there are no effects. Otherwise, if an exception is thrown by the move constructor of a
non-CopyInsertable
T
, the effects are unspecified.
我不知道你引用的帖子的作者是怎么想的,但我可以想象他们隐含地假设你想要强异常保证,所以他们想引导你进入案例(1) 或 (2).
您的示例没有任何问题。来自 std::vector::push_back
:
If T's move constructor is not noexcept
and T is not CopyInsertable into *this
, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.
std::vector
更喜欢非抛出移动构造函数,如果 none 可用,将退回到复制构造函数(抛出或不抛出)。但如果这也不可用,则它必须使用 throwing move 构造函数。基本上,vector 试图避免抛出构造函数并使对象处于不确定状态。
所以在这方面,您的示例是正确的,但如果您的移动构造函数实际上引发了异常,那么您将有未指定的行为。
TLDR只要类型是MoveInsertable,你就可以了,在这里你可以。
类型是 MoveInsertable 如果给定容器的分配器 A
,分配器的实例分配给变量 m
,指向 T*
称为 p
,并且类型为 T
的 r 值以下表达式是合式的:
allocator_traits<A>::construct(m, p, rv);
对于您的 std::vector<BufferTrio>
,您使用的是默认值 std::allocator<BufferTrio>
,因此对 construct
的调用会调用
m.construct(p, std::forward<U>(rv))
其中 U
是转发引用类型(在您的情况下是 BufferTrio&&
,右值引用)
到目前为止一切顺利,
m.construct
将使用 placement-new 就地构造成员([allocator.members])
::new((void *)p) U(std::forward<Args>(args)...)
在任何时候都不需要 noexcept
。仅出于例外保证原因。
[vector.modifiers] 表示 void push_back(T&& x);
If an exception is thrown by the move
constructor of a non-CopyInsertable T
, the effects are unspecified.
最后,
关于你的swap
(强调我的):
[container.requirements.general]
The expression a.swap(b)
, for containers a
and b
of a standard container type other than array
, shall exchange
the values of a
and b
without invoking any move, copy, or swap operations on the individual container
elements.
我已经检查了很多移动 constructor/vector/noexcept 线程,但我仍然不确定当事情应该出错时实际发生了什么。我无法按预期产生错误,所以要么我的小测试有误,要么我对问题的理解有误。
我正在使用 BufferTrio 对象的向量,它定义了一个 noexcept(false) 移动构造函数,并删除所有其他 constructor/assignment 运算符,这样就没有什么可以回退到:
BufferTrio(const BufferTrio&) = delete;
BufferTrio& operator=(const BufferTrio&) = delete;
BufferTrio& operator=(BufferTrio&& other) = delete;
BufferTrio(BufferTrio&& other) noexcept(false)
: vaoID(other.vaoID)
, vboID(other.vboID)
, eboID(other.eboID)
{
other.vaoID = 0;
other.vboID = 0;
other.eboID = 0;
}
事物编译和运行,但来自https://xinhuang.github.io/posts/2013-12-31-when-to-use-noexcept-and-when-to-not.html:
std::vector will use move when it needs to increase(or decrease) the capacity, as long as the move operation is noexcept.
或来自 优化的 C++:经过验证的提高性能的技术作者:Kurt Guntheroth:
If the move constructor and move assignment operator are not declared noexcept, std::vector uses the less efficient copy operations instead.
由于我删除了这些,我的理解是这里应该有问题。但是那个向量 运行ning 没问题。因此,我还创建了一个基本循环,将 push_backs 半百万次变成一个虚拟向量,然后将该向量与另一个单元素虚拟向量交换。像这样:
vector<BufferTrio> thing;
int n = 500000;
while (n--)
{
thing.push_back(BufferTrio());
}
vector<BufferTrio> thing2;
thing2.push_back(BufferTrio());
thing.swap(thing2);
cout << "Sizes are " << thing.size() << " and " << thing2.size() << endl;
cout << "Capacities are " << thing.capacity() << " and " << thing2.capacity() << endl;
输出:
Sizes are 1 and 500000
Capacities are 1 and 699913
仍然没有问题,所以:
我应该看到哪里出了问题,如果是,我该如何演示?
向量重新分配尝试提供异常保证,即如果在重新分配操作期间抛出异常,则尝试保留原始状态。共有三种情况:
元素类型为
nothrow_move_constructible
: 重新分配可以移动不会导致异常的元素。这是有效的情况。元素类型为
CopyInsertable
:如果类型不为nothrow_move_constructible
,这足以提供强保证,尽管在重新分配时进行了复制。这是旧的 C++03 默认行为,是效率较低的回退。元素类型既不是
CopyInsertable
也不是nothrow_move_constructible
。只要它仍然是可移动构造的,就像在您的示例中一样,向量重新分配是可能的,但不提供任何异常保证(例如,如果移动构造抛出,您可能会丢失元素)。
说明这一点的规范性措辞分布在各种重新分配职能中。例如,[vector.modifiers]/push_back
表示:
If an exception is thrown while inserting a single element at the end and
T
isCopyInsertable
oris_nothrow_move_constructible_v<T>
istrue
, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-CopyInsertable
T
, the effects are unspecified.
我不知道你引用的帖子的作者是怎么想的,但我可以想象他们隐含地假设你想要强异常保证,所以他们想引导你进入案例(1) 或 (2).
您的示例没有任何问题。来自 std::vector::push_back
:
If T's move constructor is not
noexcept
and T is not CopyInsertable into*this
, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.
std::vector
更喜欢非抛出移动构造函数,如果 none 可用,将退回到复制构造函数(抛出或不抛出)。但如果这也不可用,则它必须使用 throwing move 构造函数。基本上,vector 试图避免抛出构造函数并使对象处于不确定状态。
所以在这方面,您的示例是正确的,但如果您的移动构造函数实际上引发了异常,那么您将有未指定的行为。
TLDR只要类型是MoveInsertable,你就可以了,在这里你可以。
类型是 MoveInsertable 如果给定容器的分配器 A
,分配器的实例分配给变量 m
,指向 T*
称为 p
,并且类型为 T
的 r 值以下表达式是合式的:
allocator_traits<A>::construct(m, p, rv);
对于您的 std::vector<BufferTrio>
,您使用的是默认值 std::allocator<BufferTrio>
,因此对 construct
的调用会调用
m.construct(p, std::forward<U>(rv))
其中 U
是转发引用类型(在您的情况下是 BufferTrio&&
,右值引用)
到目前为止一切顺利,
m.construct
将使用 placement-new 就地构造成员([allocator.members])
::new((void *)p) U(std::forward<Args>(args)...)
在任何时候都不需要 noexcept
。仅出于例外保证原因。
[vector.modifiers] 表示 void push_back(T&& x);
If an exception is thrown by the move constructor of a non-
CopyInsertable T
, the effects are unspecified.
最后,
关于你的swap
(强调我的):
[container.requirements.general]
The expression
a.swap(b)
, for containersa
andb
of a standard container type other thanarray
, shall exchange the values ofa
andb
without invoking any move, copy, or swap operations on the individual container elements.