Using an object without copy and without a noexcept move constructor in a vector. What actually breaks and how can I confirm it?

我与影子孤独终老i 提交于 2021-02-04 15:08:19

问题


I've checked a lot of move constructor/vector/noexcept threads, but I am still unsure what actually happens when things are supposed to go wrong. I can't produce an error when I expect to, so either my little test is wrong, or my understanding of the problem is wrong.

I am using a vector of a BufferTrio object, which defines a noexcept(false) move constructor, and deletes every other constructor/assignment operator so that there's nothing to fall back to:

    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;
    }

Things compile and run, but from 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.

Or from Optimized C++: Proven Techniques for Heightened Performance By Kurt Guntheroth:

If the move constructor and move assignment operator are not declared noexcept, std::vector uses the less efficient copy operations instead.

Since I've deleted those, my understanding is that something should be breaking here. But things are running ok with that vector. So I also created a basic loop that push_backs half a million times into a dummy vector, and then swapped that vector with another single-element dummy vector. Like so:

    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;

Output:

Sizes are 1 and 500000
Capacities are 1 and 699913

Still no problems, so:

Should I see something going wrong, and if so, how can I demonstrate it?


回答1:


A vector reallocation attempts to offer an exception guarantee, i.e. an attempt to preserve the original state if an exception is thrown during the reallocation operation. There are three scenarios:

  1. The element type is nothrow_move_constructible: Reallocation can move elements which won't cause an exception. This is the efficient case.

  2. The element type is CopyInsertable: if the type fails to be nothrow_move_constructible, this is sufficient to provide the strong guarantee, though copies are made during reallocation. This was the old C++03 default behaviour and is the less efficient fall-back.

  3. The element type is neither CopyInsertable nor nothrow_move_constructible. As long as it is still move-constructible, like in your example, vector reallocation is possible, but does not provide any exception guarantees (e.g. you might lose elements if a move construction throws).

The normative wording that says this is spread out across the various reallocating functions. For example, [vector.modifiers]/push_back says:

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.

I don't know what the authors of the posts you cite had in mind, though I can imagine that they are implicitly assuming that you want the strong exception guarantee, and so they'd like to steer you into cases (1) or (2).




回答2:


There is nothing going wrong in your example. From 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 prefers non-throwing move constructors, and if none is available, will fall back on the copy constructor (throwing or not). But if that is also not available, then it has to use the throwing move constructor. Basically, the vector tries to save you from throwing constructors and leaving objects in an indeterminate state.

So in that regard, your example is correct, but if your move constructor actually threw an exception, then you'd have unspecified behavior.




回答3:


TLDR So long as a type is MoveInsertable, you're okay, and here you're okay.

A type is MoveInsertable if, given the container's allocator A, an instance of the allocator assigned to variable m, a pointer to T* called p, and an r-value of type T the following expression is well-formed:

allocator_traits<A>::construct(m, p, rv);

For your std::vector<BufferTrio>, you are using the default std::allocator<BufferTrio>, so this call to construct calls

m.construct(p, std::forward<U>(rv))

Where U is a forwarding reference type (in your case it's BufferTrio&&, an rvalue reference)

So far so good,

m.construct will use placement-new to construct the member in-place([allocator.members])

::new((void *)p) U(std::forward<Args>(args)...)

At no point does this require noexcept. It's only for exception guarantee reasons.

[vector.modifiers] states that for void push_back(T&& x);

If an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.


Finally, Regarding your swap (Emphasis mine):

[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.



来源:https://stackoverflow.com/questions/46874310/using-an-object-without-copy-and-without-a-noexcept-move-constructor-in-a-vector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!