在向量中使用没有副本且没有 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

仍然没有问题,所以:

我应该看到哪里出了问题,如果是,我该如何演示?

向量重新分配尝试提供异常保证,即如果在重新分配操作期间抛出异常,则尝试保留原始状态。共有三种情况:

  1. 元素类型为nothrow_move_constructible: 重新分配可以移动不会导致异常的元素。这是有效的情况。

  2. 元素类型为CopyInsertable:如果类型不为nothrow_move_constructible,这足以提供强保证,尽管在重新分配时进行了复制。这是旧的 C++03 默认行为,是效率​​较低的回退。

  3. 元素类型既不是 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.