Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 666
Xenial Avatar asked Oct 22 '17 12:10

Xenial


2 Answers

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.

like image 148
Rakete1111 Avatar answered Oct 24 '22 15:10

Rakete1111


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

like image 31
Kerrek SB Avatar answered Oct 24 '22 16:10

Kerrek SB