4

While learning remove-erase idiom, as well as understanding how std::min_element() work How to use std::min_element in C++17?. I thought to try removing minimum element from the following piece of code:

#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
    std::vector<int> v{3, 1, 4, 1, 5, 9};

    std::vector<int>::iterator result = std::min_element(v.begin(), v.end());
    std::cout << "min element at: " << std::distance(v.begin(), result);
}

There are two minimum elements in v. I tried to remove both of them with added diagnostics

int main()
{
    std::vector<int> v{3, 1, 4, 1, 5, 9};

    std::vector<int>::iterator result = std::min_element(v.begin(), v.end());

    v.erase(result); // This removes just one minimum. What if need to remove all?

    v.push_back(1); // Okay, let's add the minimum again

    std::vector<int>::iterator another_result = std::min_element(v.begin(), v.end());

    std::cout << "min element: " << *another_result  << std::endl;

    auto iter = std::remove(std::begin(v), std::end(v), *another_result);
    // If I write 1 instead of *another_result, I manage to remove all 1's. No need to use iter-1 in erase idiom then.

    std::cout << "\nWhere is my iterator pointing? It is at: " << std::distance(v.begin(), iter);

    v.erase(iter, std::end(v)); // All the minimum are gone if I use iter-1 instead of iter and use *another_result

    std::for_each(v.begin(), v.end(), [](const int& x){std::cout << x << " ";}); // Why is still "1" there?
}

link

My questions are, as highlighted in the code with the comments,

  1. Why I am able to remove all the instances of minimum by providing a literal but not a de-referenced iterator? i.e. Why does the following work?
auto iter = std::remove(std::begin(v), std::end(v), 1);

However,

  1. If I choose to stick with a de-reference iterator,
auto iter = std::remove(std::begin(v), std::end(v), *another_result);

Doesn't remove all the instances of minimum while sticking to remove-erase idiom.

William Miller
  • 9,839
  • 3
  • 25
  • 46
Sowmya
  • 91
  • 1
  • 9
  • 3
    There's a lot to read here, and it doesn't seem like a very searchable question for future use. Could you focus your question a bit? – Lightness Races in Orbit Dec 15 '19 at 20:34
  • The remove-erase idiom just uses some simple operations on the container. If you derefrence the iterator, it no longer has anything to do with the container. (I know, this is not really an answer, but hopefully it puts you on the right mindset to figure it out.) – Kenny Ostrom Dec 15 '19 at 20:50
  • After the remove, the to-be-erased portion are not guaranteed to be of any particular value. – Eljay Dec 15 '19 at 20:51
  • LRwM: Shortened the text. Kenny: I don't want to type; 1, 2, or 3 whatever is the minimum in the generic code. The question is, am I doing something wrong by passing *another_result? The answer is 'yes' but I don't know what else to put there without doing some non-generic work around. Eljay, yes I know that part. I am worried about iterator not pointing at the right location for the to-be-erased portion unless I pass a literal in this case. – Sowmya Dec 15 '19 at 21:02

2 Answers2

8

It looks like you are comparing with a reference into the vector. The element you passed in then gets moved by remove and when comparing against it a second time the reference observes some other value.

This works just fine:

int by_value = *another_result;
auto iter = std::remove(std::begin(v), std::end(v), by_value);

The third parameter of the std::remove overload you're using takes a const T&, but it's "invalidating" the reference in the process of doing its operation.

If you look at the "possible implementation" on en.cppreference

template< class ForwardIt, class T >
ForwardIt remove(ForwardIt first, ForwardIt last, const T& value)
{
    first = std::find(first, last, value);
    if (first != last)
        for(ForwardIt i = first; ++i != last; )
            if (!(*i == value))
                *first++ = std::move(*i); //here it changes the value that "value" points to
                //if you are using a reference of an element inside the vector
    return first;
}

This problem is also mentioned in the "Notes" section as:

Because std::remove takes value by reference, it can have unexpected behavior if it is a reference to an element of the range [first, last).

PeterT
  • 7,981
  • 1
  • 26
  • 34
  • Thanks PeterT, I realized this subtle thing as I just took a small break and was thinking if something is going wrong with const T& value := *another_value looks like it did. If something works with literal then it should also work with a named variable and better work with const lvalue reference. I even tried UnaryPredicate p. However, it was the hidden move that got me. Lessons learned. Thanks a lot. – Sowmya Dec 15 '19 at 21:40
  • Note that the member versions of `remove` in `list` and `forward_list` don't have this problem. – Marshall Clow Dec 15 '19 at 22:03
  • @Sowmya, the `const T& value` only says that this function (`remove`) will not modify `value` (directly), it doesn't say that someone else can't modify that value. In this case, the modification is done by the iterator values, which are **not** `const_iterator`s, and are just `iterator`s, which **do** change the values. It's a subtlety ... – ChrisMM Dec 15 '19 at 22:43
  • @ChrissMM, Yes, I realized what a horrible and stupid thing I just said and deleted my previous comment without looking at your reply. However, your reply still makes sense even for my first comment. I am saddened by the fact that constant-ness of T didn't stop me from doing stupid thing and it still moved a value that should have been casted to a constant and hence not moved. – Sowmya Dec 15 '19 at 23:33
  • 1
    It's a little misleading to say that "the reference observes some other value". It's been invalidated; it doesn't observe anything; using it has undefined behaviour. – Lightness Races in Orbit Dec 16 '19 at 02:57
  • @LightnessRaceswithMonica yeah, I often conflate what is practically happening in many implementations with what's defined in the standard. Although, I am having trouble figuring out if/how exactly this is undefined behavior. I wrote "invalidated" in quotes because this is not about the iterator being invalidated but about the value that's referenced being changed. If you could help out and point me to the way this is UB, I'll edit that in. – PeterT Dec 16 '19 at 06:33
  • @PeterT What practically happens could be all manner of chaos, in actual real life - UB isn't just a theoretical concern. Anyway, here's what you're after: https://stackoverflow.com/a/54004916/560648 – Lightness Races in Orbit Dec 16 '19 at 11:24
  • @LightnessRaceswithMonica that's not an answer to my question, the iterator is only dereferenced once upon the passing the parameter, after that only the reference exists. That answer does only talk about iterator invalidation. And it does not even say that moving elements invalidates iterators to begin with – PeterT Dec 16 '19 at 12:13
  • @PeterT Oh yeah, that's not relevant, sorry. – Lightness Races in Orbit Dec 16 '19 at 12:17
  • @PeterT Yeah I'm wrong - there doesn't appear to be any UB here – Lightness Races in Orbit Dec 16 '19 at 12:18
1

If you want to remove all the minimum values in one go, you could do something a little more odd like this:

template<class T>
void remove_min( std::vector<T> &container ) {
    if ( container.empty() ) return;
    T min_val = *std::min_element( container.begin(), container.end() );
    container.erase( std::remove( container.begin(), container.end(), min_val ), container.end() );
}

Note that the min_val is a copy first (see PeterT's answer for explanation). The above can probably be modified to work with other containers.

Keep in mind that std::remove doesn't really remove anything. The return value from the function will point to after where the new last element would be, then call the container's erase method from there to remove all the elements from that point on.

ChrisMM
  • 8,448
  • 13
  • 29
  • 48
  • TIL to be extra careful and paranoid with anything that involved the move operation. For e.g. I can still make a horrible mistake with ```` template void remove_min( std::vector &container ) { if ( container.empty() ) return; container.erase( std::remove( container.begin(), container.end(), *std::min_element( container.begin(), container.end() ) ), container.end() ); } ```` – Sowmya Dec 15 '19 at 21:42
  • Just one backtick in comments; no multiline code. ;) In what you just pasted, it has the same issue, since what the iterator is pointing to will change. Anything relating to iterators, I find, you have to be careful with :p – ChrisMM Dec 15 '19 at 21:48
  • It's a little misleading to say that "what the iterator is pointing to will change". It's been invalidated; it doesn't point to anything; using it has undefined behaviour. – Lightness Races in Orbit Dec 16 '19 at 02:58
  • @LightnessRaceswithMonica, I don't believe that's really true. I don't believe `std::remove` invalidates the iterators, since the container itself never changes, items within it are just moved. For example, `*itr = 5` doesn't change the validity of `itr`, just its contents. Granted, the only thing the standard says on validity is _Each element in the range [ret, last), where ret is the returned value, has a valid but unspecified state_, which doesn't say anything about `[first, ret]`, but since `[ret, last)` would still be valid, I think that would mean `[first, ret]` would be too? – ChrisMM Dec 16 '19 at 12:12
  • Found this quote though: _Unless otherwise specified (either explicitly or by defining a function in terms of other functions), invoking a container member function or passing a container as an argument to a library function shall not invalidate iterators to, or change the values of, objects within that container._ [26.2.1/12] Since the standard doesn't say `std::remove` invalidates the iterators, I think that means it is well defined. – ChrisMM Dec 16 '19 at 12:16