std::vector::erase exception safety - c++

I have read that std::vector erase method use move operations only if type is known to not emit exceptions due to strong exception safety. Other comments are that erase method guarantee basic or no throw exception safety depending on that if element constructor throws or not. I wasn't able to clarify that in my C++11 draft. I did test and it shows basic exception safety guarantee, it also used move constructor which was not marked as noexcept. Did I overlook something ? What is right ?

Table 100 -- Sequence container requirements in section 23.2.3 [sequence.reqmts] says:
a.erase(q)
Requires: For vector and deque, T shall be MoveAssignable.
This means that the implementation can call no operations on T except to destruct it or move assign it. Note that if the implementation move assigns T that doesn't guarantee that a move assignment operator will be called. For example T may not have a move assignment operator, and so in that case a copy assignment operator could be called. However the implementation is not allowed to copy assign the T, only move assign it.
*i = std::move(*j); // implementation can do this
*i = *j; // implementation can not do this
Furthermore 23.3.6.5 vector modifiers [vector.modifiers] says the following:
iterator erase(const_iterator position);
iterator erase(const_iterator first, const_iterator last);
Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, assignment operator, or move assignment
operator of T.
I must admit I sighed when I read this. There is clearly a minor defect here. This operation is not allowed to form any expressions that would directly construct a T. Perhaps one is constructed as an implementation detail inside an assignment operator of T, but that is of no concern to this specification. The concern is does this expression throw or not:
*i = std::move(*j); // implementation can do this. Will it throw?
If that expression (where i and j are iterators referring to a T) does not throw, then vector::erase has the no-throw guarantee. Otherwise vector::erase has the basic exception safety guarantee.
Note that for this operation, the implementation is not allowed to fall back to copy assignment if is_nothrow_move_assignable<T>::value is false. Such logic is present in other vector operations such as push_back, but not here.
Also note the Complexity specification of this same section:
Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the move assignment
operator of T is called the number of times equal to the number of
elements in the vector after the erased elements.
Restated: If you erase a range of elements that end with the end of the vector, zero move assignments will be performed, and move assignment is the only thing that might throw. So you get back the no-throw guarantee if you are erasing at the end, even if is_nothrow_move_assignable<T>::value is false.

23.3.6.5 Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, assignment operator, or move assignment operator of T.
Provided your implementation conforms to this, it may implement the erase as it sees fit. There is no implicit exception safety guarantee as far as I can tell.

Related

When could std::priority_queue::pop throw an exception

The pop() method of std::priority_queue is not declared noexcept, so in theory could throw an exception. But when might it throw an exception, and what might those exceptions be?
It could be marked nothrow, but isn't.
Why std::priority_queue::pop could* not throw
void pop();
Removes the top element from the priority queue. Effectively calls
std::pop_heap(c.begin(), c.end(), comp); c.pop_back();
c is by default an std::vector.
[vector.modifiers]/4&5
void pop_back();
4/ Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the assignment operator of T is called the number of times equal to the number of elements in the vector after the erased elements.
5/ Throws: Nothing unless an exception is thrown by the assignment operator or move assignment operator of T.
*So only the destructor of T is called and that one cannot throw because of
[requirements.on.functions]/2.4
2/ In particular, the effects are undefined in the following cases:
[...]
2.4/ if any replacement function or handler function or destructor operation exits via an exception, unless specifically allowed in the applicable Required behavior: paragraph.
Why is std::priority_queue::pop not nothrow?
Since an exception thrown from T::~T would lead to UB, the implementation can assume it cannot happen and still conform to the Standard. Another way to deal with it is to let such library functions nothrow(false) and not dealing with it.

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?
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:
The element type is nothrow_move_constructible: Reallocation can move elements which won't cause an exception. This is the efficient case.
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.
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).
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.
TLDR So long as a type is MoveInsertable, you're okay, and here you're okay.
A type is MoveInsertable if, given the container's allocator A, an instance of the allocator assigned to variable m, a pointer to T* called p, and an r-value of type T the following expression is well-formed:
allocator_traits<A>::construct(m, p, rv);
For your std::vector<BufferTrio>, you are using the default std::allocator<BufferTrio>, so this call to construct calls
m.construct(p, std::forward<U>(rv))
Where U is a forwarding reference type (in your case it's BufferTrio&&, an rvalue reference)
So far so good,
m.construct will use placement-new to construct the member in-place([allocator.members])
::new((void *)p) U(std::forward<Args>(args)...)
At no point does this require noexcept. It's only for exception guarantee reasons.
[vector.modifiers] states that for void push_back(T&& x);
If an exception is thrown by the move
constructor of a non-CopyInsertable T, the effects are unspecified.
Finally,
Regarding your swap (Emphasis mine):
[container.requirements.general]
The expression a.swap(b), for containers a and b of a standard container type other than array, shall exchange
the values of a and b without invoking any move, copy, or swap operations on the individual container
elements.

Can I trust vector::size after an exception is thrown?

I am trying to understand how exceptions affect an std::vector. More precisely, I want to check the size of the vector, when an out of memory exception is thrown.
I mean something like this:
std::vector<int> v;
try {
for(unsigned int i = 0; i < desiredSize; ++i)
v.push_back(i);
}
catch (const std::bad_alloc&) {
cerr << "Out of memory! v.size() = " << v.size() << endl;
exit(EXIT_FAILURE);
}
Is that a good approach or should I keep track of the vector's size with an independent variable of mine?
From the documentation for std::vector::push_back:
If an exception is thrown (which can be due to Allocator::allocate() or element copy/move constructor/assignment), this function has no effect (strong exception guarantee).
So in case of failure, the last push_back which caused the exception will be rolled back, but everything else will be fine: your vector will contain all of the previously pushed elements, and will be in a consistent state.
According to [vector.modifiers] (emphasis mine):
Remarks: Causes reallocation if the new size is greater than the old capacity.
Reallocation invalidates all the references, pointers, and iterators referring to the elements in the sequence.
If no reallocation happens, all the iterators and references before the insertion point remain valid.
If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of T or by any InputIterator operation there are no effects.
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.
Since your T is int (and operations on integers never throw) you can only get the out-of-memory errors from std::vector when it attempts to allocate new memory for its contents, hence this function has no effect when throwing any exception and using size() afterwards is a perfectly valid approach.
Exceptions
If an exception is thrown (which can be due to Allocator::allocate() or element copy/move constructor/assignment), this function has no effect (strong exception guarantee).
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.

Copy/move assignment in std::vector::erase() and std::deque::erase()

In the process of answering another question I stumbled upon slightly different wordings for std::vector::erase() and std::deque::erase().
This is what C++14 says about std::deque::erase ([deque.modifiers]/4-6, emphasis mine):
Effects: ...
Complexity: The number of calls to the destructor is the same as the number of elements erased, but
The number of calls to the assignment operator is no more than the lesser of the number of elements
Before the erased elements and the number of elements after the erased elements.
Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, assignment operator, or move assignment operator of T.
And here is what it says about std::vector::erase ([vector.modifiers]/3-5):
Effects: ...
Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the move assignment operator of T is called the number of times equal to the number of elements in the vector after the erased elements.
Throws: Nothing unless an exception is thrown by the copy constructor, move constructor, assignment operator, or move assignment operator of T.
As you can see, the exception specifications for both of them are the same, but for std::vector it's explicitly mentioned that move assignment operator is called.
There's also requirement for T to be MoveAssignable for erase() to work with both std::vector and std::deque (Table 100), but this doesn't imply the presence of the move assignment operator: one can define a copy assignment operator, and not define move assignment operator, and this class will be MoveAssignable.
Just in case, I checked with GCC and Clang, and indeed std::vector::erase() calls copy assignment operator if there's no move assignment operator, and std::deque::erase() does the same (DEMO).
So the question is: did I miss something, or this is an (editorial) issue in the standard?
Update:
I've submitted an LWG issue #2477.
At Lenexa meeting the issue got Immediate status with proposed resolution:
This wording is relative to N4296.
Change 23.3.3.4 [deque.modifiers]/5 to:
-5- Complexity: The number of calls to the destructor of T is the same as the number of elements erased, but the number of calls to the assignment operator of T is no more than the lesser of the number of elements before the erased elements and the number of elements after the erased elements.
Change 23.3.6.5 [vector.modifiers]/4 to:
-4- Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the move assignment operator of T is called the number of times equal to the number of elements in the vector after the erased elements.
That is, if the resolution is accepted there will be no special mention of the move assignment for std::vector::erase, and also the wording for std::deque::erase will be
clarified a bit.

Is a moved-from vector always empty?

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?
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:
The allocators are not move assigned.
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.
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.
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;
}
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.