32

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.

Community
  • 1
  • 1
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 6
    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. – Ben Voigt Apr 12 '12 at 16:23
  • @BenVoigt: Not at all a duplicate. This is asking a specific implementation related question. – Puppy Apr 12 '12 at 16:23
  • @DeadMG: Yes it is. The answers to the other question explain this completely. – Ben Voigt Apr 12 '12 at 16:23
  • @Ben Even though the answer actually explains this, the question doesn’t ask for it so I don’t think this is a duplicate: Nobody who searches for this will find the other question. (Exhibit A: yours truly.) – Konrad Rudolph Apr 12 '12 at 16:25
  • @Konrad: I found that question and answer by searching for "[c++] vector move regression" – Ben Voigt Apr 12 '12 at 16:26
  • 2
    @KonradRudolph: I don't think this is an issue. As far as I understood duplicates are kept and simply become pointers to the base question. Am I wrong ? – Matthieu M. Apr 12 '12 at 16:27
  • @Ben Furthermore, the answer is incorrect, adding `noexcept` changes nothing, and I don’t see how the compiler could even dispatch on this … it doesn’t change the signature or anything. – Konrad Rudolph Apr 12 '12 at 16:27
  • 1
    @KonradRudolph: That answer doesn't say to use `noexcept`, it says to use `throw()`. Did you try that? – Ben Voigt Apr 12 '12 at 16:28
  • 1
    @BenVoigt no, `throw()` doesn't help either. – R. Martinho Fernandes Apr 12 '12 at 16:39
  • 1
    It is possible to dispatch on no-throw thanks to the `noexcept` operator (not to be confused with `noexcept` specifications) and type traits. `std::move_if_noexcept` comes in handy though. – Luc Danton Apr 12 '12 at 16:50
  • @BenVoigt: The other answers explain the correct form. They do not explain why the incorrect form worked on GCC 4.5.1. This question, and any answer, is entirely implementation-specific. – Puppy Apr 12 '12 at 17:30
  • @DeadMG: In the (invalid) bug report I linked in my answer, the gcc team explained that the correct behavior was implemented in g++ 4.7.x, and gcc 4.5.1 didn't implement the exception-safety requirements. – Ben Voigt Apr 12 '12 at 17:40
  • Also, there is a better duplicate out there somewhere, because I remember finding a link to that bug report on SO originally. But I can't find the original any more. – Ben Voigt Apr 12 '12 at 17:43
  • The only clear explanation I saw about the behavior of gcc 4.7 is from Jonathan Wakely (libstc++ dev) here : http://stackoverflow.com/questions/13800858/stdvectorpush-back-a-non-copyable-object-gives-compiler-error – Arzar Mar 15 '13 at 15:21

3 Answers3

13

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

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • 5
    Two years later, I’m now convinced that this interpretation is wrong – it may conform to the letter of the standard but almost certainly not to the intent: `noexcept` is widely interpreted by users of C++ (including standardese experts) to be a *binding* nothrow specification, contrary to what you’ve said. In fact, library implementors widely use it as such, and while the GCC implementation used in my question might be strictly conforming, it performs sub-optimally. This is supported by the fact that subsequent versions of GCC fixed this behaviour. – Konrad Rudolph Aug 04 '14 at 08:33
8

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)
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
6

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.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Sorry but this is wrong, see updated question. `noexcept` changes nothing. – Konrad Rudolph Apr 12 '12 at 16:28
  • @KonradRudolph: Click the bug report link. – Ben Voigt Apr 12 '12 at 16:35
  • 1
    @KonradRudolph If `noexcept` changes nothing, the proper answer is not to say this answer is wrong, but to attempt `static_assert( std::is_nothrow_move_constructible::value, "" );`. – Luc Danton Apr 12 '12 at 17:05
  • @Ben The bug report (and one of the answers on the other question) make explicit reference to `noexcept`, which is why I was confused. Furthermore, using `noexcept` would be more logical, no? And just to confuse things further, **`throw()` also doesn’t work** on my machine. – Konrad Rudolph Apr 12 '12 at 17:40
  • 1
    @Luc Alas, no dice. The metafunction evaluates to `false`. Surely *this* is a bug, no? – Konrad Rudolph Apr 12 '12 at 17:48