c++language-lawyerindexoutofboundsexceptionundefined-behaviorstd

Where in the standard to I read or deduce that vector<T>::operator[] invokes UB for out-of-bound input?


On cppreference, I read the following about std::vector<T,Allocator>::operator[]:

Accessing a nonexistent element through this operator is undefined behavior.

Does the standard draft contain a similar sentence? Or, what part of the standard should I deduce this thing from?

I guess that if the standard says nothing about it, that would make it UB. But I haven't found anything about std::vector<T,Allocator>::at either, so...


Solution

  • It's specified in a very roundabout way.

    First, the members aren't specified for vector directly, but instead for the general Sequence. You can find the specification here: http://eel.is/c++draft/sequence.reqmts.

    First, the obvious part: at has the clause

    Throws: out_of_range if n >= a.size().

    So this is where at's behavior comes from.

    As for [], it says:

    Returns: *(a.begin() + n)

    So the effect is the same as of that expression. We need to follow this to find the undefined behavior. For that we need to look at the iterator requirements.

    Now the C++20 requirements are nigh-unreadable, but the C++17 requirements are still there and do apply. So:

    This is where it gets really complicated. At this point we need to piece together the specifications of vector::begin and vector::end to understand that repeated incrementing of v.begin() will eventually (as in, after v.size() applications) yield v.end(), which is specified to be the past-the-end iterator. I cannot find the explicit statement that such iterators are not dereferenceable, but http://eel.is/c++draft/iterator.requirements#general-7 specifies that the standard library assumes they are not, and in vector's case this is true.

    Therefore, v[v.size()] is equivalent to *(v.begin() + v.size()), which is equivalent to *v.end(), which is undefined behavior.

    And v[v.size()+1] is equivalent to *(v.begin() + v.size() + 1), which is equivalent to *std::next(v.end()), which is incrementing the past-the-end iterator and thus undefined behavior.