Search code examples
c++c++11vectorconstructormove

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?


Solution

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