Search code examples
c++iteratorcontainersstandardserase

Is erasing the end iterator an oversight in the standard or a design decision?


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));

Solution

  • 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);