57

I know that generally the standard places few requirements on the values which have been moved from:

N3485 17.6.5.15 [lib.types.movedfrom]/1:

Objects of types defined in the C++ standard library may be moved from (12.8). Move operations may be explicitly specified or implicitly generated. Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state.

I can't find anything about vector that explicitly excludes it from this paragraph. However, I can't come up with a sane implementation that would result in the vector being not empty.

Is there some standardese that entails this that I'm missing or is this similar to treating basic_string as a contiguous buffer in C++03?

Community
  • 1
  • 1
Billy ONeal
  • 104,103
  • 58
  • 317
  • 552
  • 1
    I think an insane but legal implementation might have `class vector { private: T* m_data; size_type m_size; size_type m_capacity; bool m_this_owns_data; };` – aschepler Jul 18 '13 at 18:05
  • 2
    @aschepler: Nope, that would be illegal. – Puppy Jul 18 '13 at 18:09
  • "I can't find anything about vector that explicitly excludes it from this paragraph. " - you don't need. "unspecified" includes situations where only a single alternative is possible. (so unless there are objects that are placed into an invalid state after move, the prefix "Unless otherwise specified" is redundant in that quote). – Johannes Schaub - litb Jul 18 '13 at 18:40
  • @DeadMG because? I suppose guarantees that two `std::vector` don't refer to the same data? – Yakk - Adam Nevraumont Jul 18 '13 at 18:43
  • @Yakk: Yes. It is illegal for any `vector` to alias storage used by another `vector` (of the same T, obviously). – Puppy Jul 18 '13 at 18:51
  • @Mooing: The standard doesn't appear to allow `vector` to do that though (excepting case 3 in Howard's answer below). This question is specific to `vector`, not talking about move semantics in the general case. – Billy ONeal Jul 19 '13 at 00:39
  • 1
    @BillyONeal: Yeah, I read the answers, the standard is quite complicated when it comes to allocators :( – Mooing Duck Jul 19 '13 at 00:49

4 Answers4

82

I'm coming to this party late, and offering an additional answer because I do not believe any other answer at this time is completely correct.

Question:

Is a moved-from vector always empty?

Answer:

Usually, but no, not always.

The gory details:

vector has no standard-defined moved-from state like some types do (e.g. unique_ptr is specified to be equal to nullptr after being moved from). However the requirements for vector are such that there are not too many options.

The answer depends on whether we're talking about vector's move constructor or move assignment operator. In the latter case, the answer also depends on the vector's allocator.


vector<T, A>::vector(vector&& v)

This operation must have constant complexity. That means that there are no options but to steal resources from v to construct *this, leaving v in an empty state. This is true no matter what the allocator A is, nor what the type T is.

So for the move constructor, yes, the moved-from vector will always be empty. This is not directly specified, but falls out of the complexity requirement, and the fact that there is no other way to implement it.


vector<T, A>&
vector<T, A>::operator=(vector&& v)

This is considerably more complicated. There are 3 major cases:

One:

allocator_traits<A>::propagate_on_container_move_assignment::value == true

(propagate_on_container_move_assignment evaluates to true_type)

In this case the move assignment operator will destruct all elements in *this, deallocate capacity using the allocator from *this, move assign the allocators, and then transfer ownership of the memory buffer from v to *this. Except for the destruction of elements in *this, this is an O(1) complexity operation. And typically (e.g. in most but not all std::algorithms), the lhs of a move assignment has empty() == true prior to the move assignment.

Note: In C++11 the propagate_on_container_move_assignment for std::allocator is false_type, but this has been changed to true_type for C++1y (y == 4 we hope).

In case One, the moved-from vector will always be empty.

Two:

allocator_traits<A>::propagate_on_container_move_assignment::value == false
    && get_allocator() == v.get_allocator()

(propagate_on_container_move_assignment evaluates to false_type, and the two allocators compare equal)

In this case, the move assignment operator behaves just like case One, with the following exceptions:

  1. The allocators are not move assigned.
  2. The decision between this case and case Three happens at run time, and case Three requires more of T, and thus so does case Two, even though case Two doesn't actually execute those extra requirements on T.

In case Two, the moved-from vector will always be empty.

Three:

allocator_traits<A>::propagate_on_container_move_assignment::value == false
    && get_allocator() != v.get_allocator()

(propagate_on_container_move_assignment evaluates to false_type, and the two allocators do not compare equal)

In this case the implementation can not move assign the allocators, nor can it transfer any resources from v to *this (resources being the memory buffer). In this case, the only way to implement the move assignment operator is to effectively:

typedef move_iterator<iterator> Ip;
assign(Ip(v.begin()), Ip(v.end()));

That is, move each individual T from v to *this. The assign can reuse both capacity and size in *this if available. For example if *this has the same size as v the implementation can move assign each T from v to *this. This requires T to be MoveAssignable. Note that MoveAssignable does not require T to have a move assignment operator. A copy assignment operator will also suffice. MoveAssignable just means T has to be assignable from an rvalue T.

If the size of *this is not sufficient, then new T will have to be constructed in *this. This requires T to be MoveInsertable. For any sane allocator I can think of, MoveInsertable boils down to the same thing as MoveConstructible, which means constructible from an rvalue T (does not imply the existence of a move constructor for T).

In case Three, the moved-from vector will in general not be empty. It could be full of moved-from elements. If the elements don't have a move constructor, this could be equivalent to a copy assignment. However, there is nothing that mandates this. The implementor is free to do some extra work and execute v.clear() if he so desires, leaving v empty. I am not aware of any implementation doing so, nor am I aware of any motivation for an implementation to do so. But I don't see anything forbidding it.

David Rodríguez reports that GCC 4.8.1 calls v.clear() in this case, leaving v empty. libc++ does not, leaving v not empty. Both implementations are conforming.

Billy ONeal
  • 104,103
  • 58
  • 317
  • 552
Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • 4
    Thank you! And TL;DR: It's possible because it's not forbidden and the library is customizable. – Potatoswatter Jul 19 '13 at 00:10
  • 2
    Howard, I don't believe the "constant time" requirement precludes an implementation from a "short vector" "optimization", at least providing that element constructors and destructors are trivial. As long as the short vector has a maximum size, the copy operation is bounded by the time it takes to copy that size, which is enough to qualify as constant-time. In that case, even the move constructor might not leave an empty vector behind. – rici Jul 19 '13 at 02:35
  • 3
    @rici: [container.requirements.general]/p10/b6 requires that no swap invalidates any iterators of containers unless otherwise specified. vector does not otherwise specify. However [string.require]/p6/pb1 does otherwise specify for string, clarified by footnote 237. The intent of all of this is to forbid "short string" optimizations for vector, but allow them for string. – Howard Hinnant Jul 19 '13 at 03:01
  • Howard: ah, yes, good point. But does that imply that an iterator into a container is not invalidated by move? Seems like the same reasoning would apply. On the other hand, I'm not sure I'd be happy to review code which made that assumption (or, for that matter, the assumption that a moved-from vector is empty). – rici Jul 19 '13 at 04:54
  • 2
    *The implementor is free to do some extra work and execute `v.clear()`[...] I am not aware of any implementation doing so.* GCC 4.8.1 does exactly this. – David Rodríguez - dribeas Jul 19 '13 at 13:20
  • @DavidRodríguez-dribeas: Thanks. I've updated the answer with this this information. – Howard Hinnant Jul 19 '13 at 14:15
  • 1
    @rici: I believe in those cases where a buffer ownership transfer is required, an iterator into the source would become a valid iterator into the destination. That being said, the standard is not clear on this, and I would not be surprised if a debugging implementation disallowed such use anyway. The libc++ debug mode (which is in its infancy) does allow the use of such a "moved" iterator. In the case where buffer ownership transfer is forbidden, it is unspecified what happens to outstanding iterators in the source. libc++ leaves them alone and GCC 4.8.1 invalidates them. – Howard Hinnant Jul 19 '13 at 14:27
  • While discussing this in another question, the statement "the moved-from vector will always be empty" in the case of having a default constructor seems incorrect to my understanding. Can't a vector of trivially destructible objects such as integers be moved in O(1) without the need for clearing the other vector? For example, `vec(vec && other) data{ other.data } {}` would work fine as destructing the integers twice (from both the moved from and moved to vectors' destructors) will not cause any issues. – Lemon Drop Aug 19 '18 at 18:55
  • @LemonDrop: Nope. A `vector` is a pointer to a heap-allocated array. The `vector` move constructor transfers ownership of the pointer to that heap-allocated array, leaving the source vector with no heap-allocated array to point at. The source `vector` isn't "cleared". Its heap pointer is set to `nullptr`, and its size and capacity are set to 0. For example: https://github.com/llvm-mirror/libcxx/blob/master/include/vector#L1267-L1285 – Howard Hinnant Aug 20 '18 at 01:01
  • 1
    @HowardHinnant Yeah of course, but it doesn't have to be implemented that way like I said, as there's nothing in the standard which requires it and as I said you can still implement the move properly for trivially destructible types in O(1) without such things. – Lemon Drop Aug 20 '18 at 01:15
  • @LemonDrop: You may know how to do that, but I don't. – Howard Hinnant Aug 20 '18 at 13:07
  • I have a question regarding the move constructor. If the allocators are such that they don't match equal i.e. if we were to move assign we would fall under case Three, wouldn't the implementation of move constructor cause undefined behavior? We moved the pointer that belonged to the moved from object but allocator somehow need to transfer ownership of the memory, which may not be possible in some cases. – DDG Sep 01 '23 at 03:15
  • Unequal allocators are not a problem for the move constructor because there is only one of them: The lhs isn't constructed yet (has no allocator) and the rhs has only one allocator which is equal to itself. So the allocator gets transferred to the lhs along with the memory that it owns, leaving the rhs with a moved-from allocator, which is still required to be equal, and no memory which needs deallocating. – Howard Hinnant Sep 01 '23 at 12:09
5

While it might not be a sane implementation in the general case, a valid implementation of the move constructor/assignment is just copying the data from the source, leaving the source untouched. Additionally, for the case of assignment, move can be implemented as swap, and the moved-from container might contain the old value of the moved-to container.

Implementing move as copy can actually happen if you use polymorphic allocators, as we do, and the allocator is not deemed to be part of the value of the object (and thus, assignment never changes the actual allocator being used). In this context, a move operation can detect whether both the source and the destination use the same allocator. If they use the same allocator the move operation can just move the data from the source. If they use different allocators then the destination must copy the source container.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • Afraid that this would actually be illegal, because iterator invalidation. – Puppy Jul 18 '13 at 18:10
  • 4
    @DeadMG: This is the second comment in a row about *iterator invalidation*, would you mind explaining the particular point you have in mind ? – Matthieu M. Jul 18 '13 at 18:17
  • Move assignment must change the allocator being used if `allocator_traits::propagate_on_container_move_assignment::value‌​` is true. – Billy ONeal Jul 18 '13 at 18:23
  • @BillyONeal: and it must not change the allocator if `allocator_traits::propagate_on_container_move_assignment::value‌`,... ? – David Rodríguez - dribeas Jul 18 '13 at 18:27
  • @DavidRodríguez-dribeas: If `allocator_traits::propagate_on_container_move_assignment::value‌​` is false then the behavior is undefined unless the allocators are equal. – Billy ONeal Jul 18 '13 at 18:29
  • 1
    @BillyONeal: I am not 100% sure of that. The wording specifically singles out `swap` from the rest of the operations in which the allocator might be *changed*. 23.2.1/7 states when the allocator can change, and it mentions that `swap` is undefined unless the allocator propagates on swap or the allocators in both objects are the same. – David Rodríguez - dribeas Jul 18 '13 at 18:43
  • Sorry, I had to rush off. See my answer for the iterator invalidation thing. – Puppy Jul 18 '13 at 18:49
  • @DeadMG: Look at Table 99, requirements for move-assignment. It clearly states that the elements in the destination container can be *move-assigned* from the elements in the original container, in which case the original container. In the case where the contained element does not have a move assignment, the elements in the destination container will be **copied**. – David Rodríguez - dribeas Jul 18 '13 at 19:05
  • @BillyONeal: I checked the standard, and I believe I am right. The standard allows for *copying* (or *moving*) the elements (rather than the data structure) if the trait is not set. At any rate if the `propagate_on_container_move_assignment` is not true, the operation is well defined. – David Rodríguez - dribeas Jul 18 '13 at 19:07
  • @David: 23.2.1/7: The behavior of a call to a container’s swap function is undefined unless the objects being swapped have allocators that compare equal or allocator_traits::propagate_on_container_swap::value is true. – Billy ONeal Jul 18 '13 at 20:38
  • @BillyONeal: Yes, it is undefined behavior to call `swap` if the allocators differ and don't propagate. But there is a different trait for move, and different rules for move assignment. Someone asked in the std-discussion group and Howard Hinnant provided sample code that demonstrates (that in the implementation he uses) that the data might be element-wise moved, i.e. the original container is left with N moved-from elements. – David Rodríguez - dribeas Jul 18 '13 at 20:52
  • @David: Yes, and those rules say that the same operation is applied to the allocator. They don't permit swapping of the allocator. – Billy ONeal Jul 18 '13 at 20:54
  • @BillyONeal: I am not sure I follow your reasoning or why you are concerned with the allocator being swapped (unless you are referring to the last sentence of the answer's first paragraph). – David Rodríguez - dribeas Jul 18 '13 at 20:56
  • @David: I'm saying the standard never allows the container to swap the allocator in a move constructor or move assignment operator. As a result, move assignment or move construction can't be swaps. – Billy ONeal Jul 18 '13 at 21:31
  • @BillyONeal: Ok, as of the question of whehter move can be implemented as swap. If the allocator does not propagate but compares equal I believe you can use swap instead of move, that is, the memory block gets moved, the allocator does not get propagated. Admitedly that is a corner case, although not too far from gcc 4.8's implementation of move assignment in that particular code path (it calls a helper function *swap-data* and optionally propagates the allocator if the propagate on assignment trait is set. – David Rodríguez - dribeas Jul 18 '13 at 21:36
  • @David: Nope, not allowed. N3485 23.2.1 [container.requirements.general]/7: Move constructors obtain an allocator by move construction from the allocator belonging to the container being moved. [ ... ] Allocator replacement is performed by copy assignment, move assignment, or swapping [ ... ] within the implementation of the **corresponding container operation**. – Billy ONeal Jul 18 '13 at 22:25
  • @BillyONeal: You ommitted the part where it says *allocator replacement is performed [...] **only if** [the respective propagate_on_\* trait is true]*. If the relative trait is not set, the allocator won't change during move-assignment nor during swap which is well defined if the allocators on both source and destination compare equal. – David Rodríguez - dribeas Jul 19 '13 at 13:01
4

In a lot of situations, move-construction and move-assignment can be implemented by delegating to swap - especially if no allocators are involved. There are several reasons for doing that:

  • swap has to be implemented anyway
  • developer efficiency because less code has to be written
  • runtime efficiency because fewer operations are executed in total

Here is an example for move-assignment. In this case, the move-from vector will not be empty, if the moved-to vector was not empty.

auto operator=(vector&& rhs) -> vector&
{
    if (/* allocator is neither move- nor swap-aware */) {
        swap(rhs);
    } else {
        ...
    }
    return *this;
}
nosid
  • 48,932
  • 13
  • 112
  • 139
  • I don't think that is legal due to allocator requirements. Specifically this makes the assignment operator sensitive to `allocator_traits::propagate_on_container_swap::value`, while the standard permits it only to be sensitive to `allocator_traits::propagate_on_container_move_assignment::value` – Billy ONeal Jul 18 '13 at 18:22
  • @BillyONeal: You are right. Nevertheless, the example shows that there can be valid implementations, that swap the data structures, so that the moved-from vector is not empty. I have updated my answer to respect the allocator traits. – nosid Jul 18 '13 at 19:53
  • No, that still doesn't work. `propagate_on_container_move_assignment` requires that the allocator itself be move-assigned. Your example above swaps the allocator which is not permitted. – Billy ONeal Jul 18 '13 at 20:37
  • (`at` would be a template parameter, not necessarily a `std::allocator_traits`.) – aschepler Jul 19 '13 at 00:52
1

I left comments to this effect on other answers, but had to rush off before fully explaining. The result of a moved-from vector must always be empty, or in the case of move assignment, must be either empty or the previous object's state (i.e. a swap), because otherwise the iterator invalidation rules cannot be met, namely that a move does not invalidate them. Consider:

std::vector<int> move;
std::vector<int>::iterator it;
{
    std::vector<int> x(some_size);
    it = x.begin();
    move = std::move(x);
}
std::cout << *it;

Here you can see that iterator invalidation does expose the implementation of the move. The requirement for this code to be legal, specifically that the iterator remains valid, prevents the implementation from performing a copy, or small-object-storage or any similar thing. If a copy was made, then it would be invalidated when the optional is emptied, and the same is true if the vector uses some kind of SSO-based storage. Essentially, the only reasonable possible implementation is to swap pointers, or simply move them.

Kindly view the Standard quotes on requirements for all containers:

X u(rv)    
X u = rv    

post: u shall be equal to the value that rv had before this construction

a = rv

a shall be equal to the value that rv had before this assignment

Iterator validity is part of the value of a container. Although the Standard does not unambiguously state this directly, we can see in, for example,

begin() returns an iterator referring to the first element in the container. end() returns an iterator which is the past-the-end value for the container. If the container is empty, then begin() == end();

Any implementation which actually did move from the elements of the source instead of swapping the memory would be defective, so I suggest that any Standard wordings saying otherwise is a defect- not least of which because the Standard is not in fact very clear on this point. These quotes are from N3691.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • 1
    What's `std::optional`? What's `std::none`? Is this C++1y? – Angew is no longer proud of SO Jul 18 '13 at 18:49
  • 1
    @Angew: Yes. I could have used some other means to destroy the source vector, but hey, I'm lazy and most people are quite familiar with the semantics of `optional`. – Puppy Jul 18 '13 at 18:50
  • 1
    I did, however, edit my answer so that it's slightly more obtuse but does not use `optional`. – Puppy Jul 18 '13 at 18:55
  • 1
    Where does the standard require `it` to remain valid? All I could find was "invoking a container member function ... shall not invalidate iterators to, ..., objects within that container." After `x` is destroyed, there are no more objects within it, so no iterator can refer to them. – Angew is no longer proud of SO Jul 18 '13 at 18:58
  • 2
    why must it always be empty? Can the source vector not first move its pointer to the destination vector (therby keeping the invalidation guarantees), and then adding one or more elements again to itself? (starting with a new buffer from scratch). While in release programs that wouldn't be sensible behavior, I guess, I imagine this to be a useful part of programs-bug finders that try to find program bugs that rely on "incorrect assumptions about Standard library move constructors". So is this specified explicitly anywhere? – Johannes Schaub - litb Jul 18 '13 at 18:59
  • 1
    @Johannes: Strictly, you could do (but that would be utterly pointless, and it's questionable as to what you would construct those elements from, as they have no requirements except movable and you can't move one of the existing elements). Not to mention the complexity requirement of moving is O(1). – Puppy Jul 18 '13 at 19:00
  • 1
    The complexity requirement of move construciton is O(1). Move assignment can be linear. – Angew is no longer proud of SO Jul 18 '13 at 19:01
  • 1
    I assume that the iterator in the outer scope is called `it` instead of `x`. Other than that, what clause in the standard guarantees that you can use `it` in the last line? In particular Table 99, `a = rv;` requirements are: *If `allocator_traits::propagate_on_container_move_assignment::value` is `false`, `T` is `MoveInsertable` into `X` and `MoveAssignable`. All existing elements of `a` are either **move assigned to** or destroyed*. – David Rodríguez - dribeas Jul 18 '13 at 19:02
  • In the case where the elements of `a` are *moved-assigned*, the original container can be kept with the same number of *possibly emptied* elements. I am quite positive that you got this one wrong. – David Rodríguez - dribeas Jul 18 '13 at 19:03
  • @David: Nope. If the new container does not directly take the source container's memory, you fall afoul of the iterator invalidation. The sample in my answer clearly shows this. Whether or not that requirement is real or I imagined it is another question; but if it is true, then your implementation is illegal because the iterators I hold into the source container would no longer be valid. – Puppy Jul 18 '13 at 19:09
  • 3
    I would have thought `move = std::move(x);` can invalidate `it`. Looks like you're implying `it` is now an iterator to the first element of `move`. But I can't find support in the Standard for either. – aschepler Jul 18 '13 at 19:10
  • @DeadMG: Adding two elements is O(1). – aschepler Jul 18 '13 at 19:12
  • 3
    @DeadMG: *you fall afoul of the iterator invalidation*. What rule are you referring to? `swap` has specific requirements different from those of move-assignment. The requirements on move-assignment clearly state that the elements can be *move-assigned* (note the elements, not the container's data structure) if the allocator does not propagate on move assignment. That would contradict any rule requiring that the iterators remain valid and refer to the destination container. – David Rodríguez - dribeas Jul 18 '13 at 19:17
  • @David: The Standard wording is not as clear as it could be, but I have a pretty reasonable case here. – Puppy Jul 18 '13 at 19:30
  • 2
    @DeadMG: *Iterator validity* is **not** part of the **value** of a container. Borrowing your own example: `C outer; C::iterator it; { C inner; it=inner.end(); swap(outer,inner); } /* it? */`. After the block completes, `it` might or not be valid. `C a = ...; C b = a; C::iterator it = b.begin(); b.reserve(b.size()*2); assert(a==b);` yet the iterator has been invalidated... – David Rodríguez - dribeas Jul 18 '13 at 19:40
  • What you are saying makes little sense. If the iterators were part of the value of the container, then `operator==` would never be `true` for two different containers. There are certain characteristics of the containers that are explicitly considered not to be part of the value. For example the capacity of the container. – David Rodríguez - dribeas Jul 18 '13 at 19:43
  • @David: That's not true. The iterators can *refer to* a container value. If the container value is unchanged, then they should still be valid, even if they are not a part of it. – Puppy Jul 18 '13 at 19:50
  • 1
    Nothing says that a past-the-end iterator might be invalidated when its container is destroyed either, but I sure wouldn't use it. – aschepler Jul 18 '13 at 19:52
  • 4
    @DeadMG: The *value* of a `std::vector` does not change during a `reserve()` operation, but iterators become invalidated. Two vectors with different capacities, but same size, and same set of elements in exactly the same order **are equal**. `vector a = f(), b = a; iterator it = b.begin(); b.reserve(2*a.size());` The reserve operation does not change the *value* of `b` but it surely invalidates iterators. – David Rodríguez - dribeas Jul 18 '13 at 20:00
  • Even if your interpretation of iterator validity is valid, there are ways to implement iterators such that `swap` preserves existing iterators but `move` does not. The requirement that `begin` returns an iterator to the first element does not directly imply that it remains valid as long as that element still resides at the same address. – Potatoswatter Jul 19 '13 at 00:16
  • "The iterator is invalidated" doesn't mean it _necessarily_ has a changed value, or points to invalid memory, or points to an element with a new value. It just means "bugger off". – Lightness Races in Orbit Jul 22 '13 at 20:04