Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does std::vector::reserve guarantee that the implementation will not invalidate iterators in this case?

Here is a function which intends to:

a) accept a vector of int

b) for each int in the input vector, append the inverse of this int

preconditions: none

postconditions: returned vector's size() is exacly 2 * the input vector's size.

Note that the vector is modified in-place.

Question:

Is this function strictly defined behaviour vis-a-vis iterator invalidation during the transform?

Bonus:

Is there a better/more succinct/robust way to write it?

Code:

std::vector<int> append_negatives(std::vector<int> v)
{
    v.reserve(v.size() * 2);
    std::transform(begin(v), end(v), 
                   back_inserter(v), 
                   [](auto&& x) { return -x; });
    return v;
}
like image 836
Richard Hodges Avatar asked Sep 09 '17 16:09

Richard Hodges


People also ask

Does std :: move invalidate iterators?

For reference, std::vector::swap does not invalidate iterators.

What does std :: vector Reserve do?

std::vector class provides a useful function reserve which helps user specify the minimum size of the vector.It indicates that the vector is created such that it can store at least the number of the specified elements without having to reallocate memory.

How do you avoid iterator invalidation in C++?

To avoid invalidation of references to elements you can use a std::deque if you do not insert or erase in the middle. To avoid invalidation of iterators you can use a std::list.

What invalidates an iterator?

An Iterator becomes invalidate when the container it points to changes its shape internally i.e. move elements from one location to another and the initial iterator still points to old invalid location. Iterator invalidation in vector happens when, An element is inserted to vector at any location.


2 Answers

As long as you don't exceed preallocated capacity no reallocation should happen and no references or iterators referring to vector items should get invalidated. However since iterator returned by end() does not refer to any vector items it may still get invalidated.

23.3.11.3 vector capacity [vector.capacity]

  1. ...No reallocation shall take place during insertions that happen after a call to reserve() until the time when an insertion would make the size of the vector greater than the value of capacity()

...

23.3.11.5 vector modifiers [vector.modifiers]

  1. ...If no reallocation happens, all the iterators and references before the insertion point remain valid.
like image 139
user7860670 Avatar answered Oct 04 '22 04:10

user7860670


The part about the iterator invalidation rules has already been covered, so I'll take this occasion to humbly question the need to use all this machinery (with its delicate rules) for such a trivial problem. You may also consider this a stab at the

Bonus:

Is there a better/more succinct/robust way to write it?

part of the question.


Honestly, I'd sidestep the problem completely with the advanced technology of for loops. KISS!

std::vector<int> append_negatives(std::vector<int> v) {
    size_t sz = v.size();
    v.resize(sz*2);
    for(size_t i = 0; i < sz; ++i) v[i+sz] = -v[i];
    return v;
} 

Not everything must be obfuscated in a deep cloak of STL algorithms. This is both shorter, IMO more readable and avoids the headaches about iterators invalidation.

Also, unless compilers have gotten extra smart since last time I checked, this is significantly faster, as avoids all the overhead of push_back (check if capacity is enough and increment size at each iteration), which is replaced with straight pointer access (one assembly instruction); the slim and simple loop body gives better chances for automatic unrolling. I expect this to be faster to compile as well, as we avoid instantiating a bunch of templates.

Mind you, the speed advantage/no invalidation headache can be obtained even with the transform benemoth:

std::vector<int> append_negatives(std::vector<int> v) {
    size_t sz = v.size();
    v.resize(sz * 2);
    auto beg = begin(v);
    std::transform(beg, beg + sz, beg + sz, 
                   [](int x) { return -x; });
    return v;
}

but I fail to see any advantage of this kind of overly verbose code over a plain for loop, unless you are paid by the number of "cool" C++ features you manage to squeeze in your code.


@MikeMB asks about types that are costly to default-construct; in that case, an explicit push_back is fine:

std::vector<int> append_negatives(std::vector<int> v) {
    size_t sz = v.size();
    v.reserve(sz*2); // optional
    for(size_t i = 0; i < sz; ++i) v.push_back(-v[i]);
    return v;
}

You could also use a range-based for loop if the transform in the original post was valid:

std::vector<int> append_negatives(std::vector<int> v) {
    v.reserve(v.size()*2);
    for(int &x: v) v.push_back(x);
    return v;
}

which is way nicer on the eyes than the transform-based solution, but I see in the comments that there's still no consensus about the end iterator invalidation, so this would be just as invalid.

like image 32
Matteo Italia Avatar answered Oct 04 '22 04:10

Matteo Italia