4

The standard library containers allow us to erase ranges denoted by iterators first and last.

std::vector<foo> bar;
//         first it     last it
bar.erase(bar.begin(), bar.end());

The standard says that the first iterator must be valid and dereferenceable, whereas last only needs to be valid. However if first == last then first does not need to be dereferenceable because the erase is a no-op. This means the following is legal:

bar.erase(bar.end(), bar.end());

However if I only wish to erase one element rather than a range the iterator must be valid and dereferenceable making the following undefined behaviour:

bar.erase(bar.end());

Why isn't this just a no-op? Is it an oversight by the standards committee that will be addressed in a future revision of the language or is it a deliberate design decision that I'm not seeing the point of?

As far as I can see it provides no benefits but creates extra headaches when performing something like the following:

bar.erase(std::find(bar.begin(), bar.end(), thing));
Fibbs
  • 1,350
  • 1
  • 13
  • 23
  • 5
    It would have to compare the iterator, so everyone who doesn't ever pass in an end iterator would now have an extra useless check done. The range one needs to compare the two anyway to know when to stop erasing. – chris Apr 17 '16 at 19:28
  • 1
    @chris: That's right, and it should be an _answer_. – Lightness Races in Orbit Apr 17 '16 at 19:39
  • @LightnessRacesinOrbit, It was more of a guess, really. It's one small check. – chris Apr 17 '16 at 19:40
  • @chris: Well either way it's an answer not a comment so please post it where it can be peer reviewed and possibly accepted! Thanks – Lightness Races in Orbit Apr 17 '16 at 19:43
  • It makes sense I guess but it doesn't doesn't help keep things consistent. I'd accept this as an answer if you could quote something/someone official stating that the extra check is the rationale for the behaviour. – Fibbs Apr 17 '16 at 19:46
  • Does this answer your question? [Erasing vector::end from vector](https://stackoverflow.com/questions/9590117/erasing-vectorend-from-vector) – Ken Y-N Nov 26 '21 at 07:42

2 Answers2

3

C++ has a habit of leaving any extra work out in its standard library, also known as "You don't pay for what you don't use". In this case, the single iterator version would need to compare the iterator to the end iterator. For the majority of cases, where this check is redundant, this is extra work that isn't being used, albeit a small bit of work.

erase(iterator it):
    if it != end: // we can't avoid this check now
        // erase element

In the case of the overload taking a range, it stops naturally before the end. A naive implementation might be as follows:

erase(iterator first, iterator last):
    while first != last:
        erase(first++);

It needs some way of knowing when to stop erasing. In some cases, it could be smarter, such as memmoveing a block of memory to overwrite the erased memory without ever branching, but that would only happen in specific scenarios.


Also note that it's much easier to build the checked version from this than the other way around:

checked_erase(Container cont, iterator it):
    if it != cont.end():
        cont.erase(it);
chris
  • 60,560
  • 13
  • 143
  • 205
  • I've accepted the answer because it is the most likely reason for the behaviour and no other answers seem forthcoming. I do however struggle to think of a situation where I would be handling a single iterator and know for sure that it was not the end() without checking. – Fibbs Apr 17 '16 at 20:05
  • @Fibbles, You erase the maximum element from a container you know isn't empty. Or the first element. Maybe you have a library that returns `optional` instead of the end and you do (simplified operation) `opt.do_if_present(it -> cont.erase(it));` When designing a library, it's typically hard to foresee just how broad the use cases are, too. It's often nice to account for uses that you can't think of, but someone will inevitably have. – chris Apr 17 '16 at 20:08
1

Why isn't this just a no-op?

Why would it be a no-op? You're calling a method specifically designed to remove a single element, and calling it in such a way that it cannot remove any element. That doesn't make any sense. You might argue that it should throw an exception instead of having undefined behaviour, but I don't see any valid argument in your question for making it well-defined as doing nothing, so I see no reason to think it might be an oversight.

Your example is not convincing to me: as long as the item is unique in the vector, it's already easily expressed in a simple form by using std::remove rather than std::find. If the item is not unique, then make it more explicit how many items you wish to remove.

Look at it another way: erase(it) is equivalent to erase(it, it+1) for all iterators. Including the end iterator. Making a special exception to have erase(it) well-defined for the end iterator where erase(it, it+1) would be undefined would be introducing an inconsistency.

  • Consider a vector where an element may either be unique or not exist. Using std::remove will search the entire vector needlessly even after it finds the unique element, whereas std::find will stop as soon as it finds the element. Of course the correct method would probably be something like std::unique coupled with erase. The std::find example was just the first thing I thought of. – Fibbs Apr 17 '16 at 19:59
  • @Fibbles You could also use `remove_if` with a predicate that stops comparing once a match is found. –  Apr 17 '16 at 20:04