Why does reallocating a vector copy instead of mov

2019-01-08 21:44发布

问题:

Possible Duplicate:
How to enforce move semantics when a vector grows?

insert, push_back and emplace(_back) can cause a reallocation of a std::vector. I was baffled to see that the following code copies the elements instead of moving them while reallocating the container.

#include <iostream>
#include <vector>

struct foo {
    int value;

    explicit foo(int value) : value(value) {
        std::cout << "foo(" << value << ")\n";
    }

    foo(foo const& other) noexcept : value(other.value) {
        std::cout << "foo(foo(" << value << "))\n";
    }

    foo(foo&& other) noexcept : value(std::move(other.value)) {
        other.value = -1;
        std::cout << "foo(move(foo(" << value << "))\n";
    }

    ~foo() {
        if (value != -1)
            std::cout << "~foo(" << value << ")\n";
    }
};

int main() {
    std::vector<foo> foos;
    foos.emplace_back(1);
    foos.emplace_back(2);
}

On my specific machine using my specific compiler (GCC 4.7) this prints the following:

foo(1)
foo(2)
foo(foo(1))
~foo(1)
~foo(1)
~foo(2)

However, when deleting the copy constructor (foo(foo const&) = delete;), the following (expected) output is generated:

foo(1)
foo(2)
foo(move(foo(1))
~foo(1)
~foo(2)

Why is that? Would’t moving generally be more efficient, or at least not much less efficient, than copying?

It bears noting that GCC 4.5.1 does the expected thing – is this a regression in GCC 4.7 or is it some deviously clever optimisation because the compiler sees that my object is cheap to copy (but how?!)?

Also note that I made sure that this is caused by reallocation, by experimentally putting a foos.reserve(2); in front of the insertions; this causes neither copy nor move to be executed.

回答1:

The short answer is that I think @BenVoigt is basically correct.

In the description of reserve (§23.3.6.3/2), it says:

If an exception is thrown other than by the move constructor of a non-CopyInsertable type, there are no effects.

[And the description of resize in §23.3.6.3/12 requires the same.]

This means that if T is CopyInsertable, you get strong exception safety. To assure that, it can only use move construction if it deduces (by unspecified means) that move construction will never throw. There's no guarantee that either throw() or noexcept will be necessary or sufficient for that though. If T is CopyInsertable, it can simply choose to always use copy construction. Basically, what's happening is that the standard requires copy construction-like semantics; the compiler can only use move construction under the as-if rule, and it's free to define when or if it'll exercise that option.

If T is not CopyInsertable, reallocation will use move construction, but exception safety depends on whether T's move constructor can throw. If it doesn't throw, you get strong exception safety, but if it throws, you don't (I think you probably get the basic guarantee, but maybe not even that and definitely no more).



回答2:

It's not a regression, it's a bug fix. The standard specifies that std::vector will only prefer an element move constructor which is non-throwing.

See also this explanation and this bug report.

This question is also relevant.



回答3:

Tip-of-trunk clang + libc++ gets:

foo(1)
foo(2)
foo(move(foo(1))
~foo(2)
~foo(1)

If you remove the noexcept from the move constructor, then you get the copy solution:

foo(1)
foo(2)
foo(foo(1))
~foo(1)
~foo(2)
~foo(1)