简体   繁体   中英

Arithmetic on end() iterator

Let A be a std::vector<double> ,

Is this well-defined?

if(!A.empty())
    std::vector<double>::iterator myBack = A.end() - 1;

Is the end iterator only good for equalities and inequalities checks? Or I can perform some pointer arithmetic as long as I remain in the container?

On my platform this code works. I'm wondering if this is portable.

It is perfectly valid as vector::iterator is a random access iterator. You can perform arithmetic operations on it and it is not platform dependent.

std::vector<double>::iterator it = A.end();
while (it != A.begin()){
    --it; //this will skip A.end() and loop will break after processing A.front()
    //do something with 'it'
}

But A.end() refers to theoretical past-the-end element, so it does not point to an element and thus shall not be dereferenced. So best practice is to use reverse iterator instead of decrementing end iterator.

for(std::vector<double>::reverse_iterator it = A.rbegin(); it != A.rend(); ++it) {
    //do something with 'it'
}

These two loops do the same thing, second is just understandable, cleaner way to do it.

It's almost safe if you are mindful of some exceptional cases:

A.end() gives you an iterator denoting the position just beyond the end of the std::vector . You should not attempt to dereference it.

If the vector has zero elements then A.end() - 1 is not well-defined. In all other cases it is and you can indeed perform pointer arithmetic so long as you are in the container bounds. Note that the standard guarantees that the std::vector data are contiguous and packed in exactly the same way as an C++ array of the contains type. The only exception is std::vector<bool> which behaves differently due to a standards-specified tight packing specialisation. (Note well that sizeof(bool) is not guaranteed to have a particular value by the standard).

If I were you I'd use A.rbegin() to access the rightmost element and check the return value before proceeding and stick to the iterator formulation. It's all too easy to forget the std::vector<bool> specialisation.

I realize this question is a bit old but I was directed here regarding end() - 1 , and I found the existing answers and comments to be informative and reasonable, but unconvincing in lack of citations, and also I wasn't sure if they were specific to vector . So I dug up as much concrete information as I could to convince myself that the answers here were correct.

This post represents my research to confirm the answers, and is basically my notes, but I tried to make it as coherent as possible and I thought it might be useful. If anything here is off, corrections would be greatly appreciated.


Restating The Answers

The TL;DR here is yes, the answers here are correct, not just for vector , but in the more general case as well:

If the container's iterator types satisfy BidirectionalIterator (and therefore provide decrement operations), then the following will always be valid, for any container type, where e is initialized to the return value of container.end() :

  • If !container.empty() then --e is valid.
  • If !container.empty() then ++(--e) == container.end() is true.

If the iterators also satisfy RandomAccessIterator , then these more general statements are valid:

  • e - n and e -= n for any integer n in [ 0, container.size() ]
  • e + n and e += n for any integer n in [ - container.size() , 0 ]

And so, the vector example in the OP is not only fine, as the other answers also state, but it's well-defined and guaranteed to be fine for any container type.


Reasoning

So now here is the bit that I felt was missing. First, from the Container requirements:

expression return type semantics conditions complexity
a.end() (const_)iterator iterator to one past the last element of a Constant

This says "one past the last element". However, does this mean end() is decrementable? We need to be sure. The items below are significant here and I've numbered them for reference:

  1. Container : The " end() returns one past the end of a " requirement mentioned above.
  2. RandomAccessIterator : i - n , defined in terms of -= , no constraints given.
  3. RandomAccessIterator : r -= n , defined in terms of += , no constraints given.
  4. RandomAccessIterator : r += n , defined in terms of --r for n < 0 , no constraints given.
  5. BidirectionalIterator : --a
    • Precondition: a is decrementable → there exists b such that a == ++b .
    • Postcondition: a is dereferenceable.
    • Postcondition: --(++a) == a
    • Postcondition: if --a == --b then a == b
    • Postcondition: a and --a are the same iterator instance.
  6. BidirectionalIterator : Notes: "A bidirectional iterator does not have to be dereferenceable to be decrementable (in particular, the end iterator is not dereferenceable but is decrementable)".
  7. Container : States that size() is semantically equivalent to std::distance(begin(), end())
  8. distance : Returns the number of increments to get from first to last .

Breaking this down:

The precondition for (5) states that for --a to work, a must be decrementable , and goes on to define that an iterator a is decrementable if there exists a b such that ++ b == a .

(1)'s "one past the end" language seems to imply that if b is an iterator to the last element in the container, then ++ b == end() . More convincingly, though, (7) shows that std::distance(begin(), end()) must work, and (8) therefore implies that the iterator returned by begin() must be able to be repeatedly incremented until it equals end() , which means that for a non-empty container, at some point there must exist a b such that ++ b == end() .

Combining these two, then, shows that end() is always decrementable if !empty() , because there is always a b such that ++ b == end() (otherwise distance(begin(), end()) — and therefore size() — would not meet its semantic requirements), which is the definition of decrementability. Also note that (6) explicitly states that a decrementable iterator need not be dereferenceable , and has a note about the decrementability of the end iterator.

Furthermore, since end() is decrementable when !empty() , then (where e is initialized to the return value of container.end() ):

  • -- e is valid, from (5).
  • e += n for n <= 0 is valid, from (4).
  • e -= n for n >= 0 is valid, from (3).
  • e - n for n >= 0 is valid, from (2).
  • Since += , -= , and - (for sign of n indicated above) are all semantically defined in terms of repeatedly applying -- , this constrains n to be within the container's size, since begin() is not decrementable (by definition of decrementability) and eventually the iterator must hit begin() .

Therefore the - 1 in the OP is valid (from (2)) as long as there is at least 1 element before the iterator it's being applied to.

Decrementability vs. dereferenceability : Note that there is a difference. (6) points out that the concepts are separate. Decrementability implies that --i is valid, dereferenceability implies that *i and i-> are valid. In the OP's vector example, while end() is decrementable, it is not dereferenceable ( vector::end() explicitly states this).


Code

Oh, yeah, also I wrote a test program just as a sanity check:

#include <boost/core/demangle.hpp>
#include <version>
#if __has_include(<array>) && (__cplusplus >= 201103L)
#  include <array>
#  define HAVE_ARRAY 1
#endif
#include <vector>
#include <deque>
#include <list>
#include <set> // + multiset
#include <map> // + multimap
#if __has_include(<span>) && (__cpp_lib_span >= 202002L)
#  include <span>
#  define HAVE_SPAN 1
#endif
#include <typeinfo>
#include <cassert>
#include <cstdio>

#if (__cpp_constexpr < 200704L)
#  define constexpr
#endif

using namespace std;

constexpr const int MAGIC = 42;

int extract (const int &v) {
    return v;
}

int extract (const pair<int,int> &v) {
    assert(v.first == v.second);
    return v.first;
}

template <typename C> struct decrementable_end_tester {
    C container;
    decrementable_end_tester ();
    void test () {
        printf("%s...\n", boost::core::demangle(typeid(C).name()).c_str());
        assert(!container.empty());
        {
            typename C::iterator b = container.begin();
            typename C::iterator e = container.end();
            assert(b == --e);
            assert(extract(*e) == MAGIC);
            assert(container.end() == ++e);
        }
        {
            typename C::iterator b = container.begin();
            typename C::iterator e = container.end(); 
            assert(e == ++b);
            assert(container.begin() == --b);
            assert(extract(*b) == MAGIC);
        }
    }
};

// i thought templating that would make initialization simpler but i'm not really
// that great with templates so i dunno if i got the most out of it:

template <typename C> decrementable_end_tester<C>::decrementable_end_tester () {
    container.insert(container.end(), MAGIC);
}

#if HAVE_ARRAY
template <> decrementable_end_tester<array<int,1> >::decrementable_end_tester () {
    container[0] = MAGIC;
}
#endif

#if HAVE_SPAN
static int span_buffer = ~MAGIC;
template <> decrementable_end_tester<span<int,1> >::decrementable_end_tester () 
    : container(&span_buffer, 1)
{
    container[0] = MAGIC;
}
#endif

template <> decrementable_end_tester<map<int,int> >::decrementable_end_tester () {
    container.insert(make_pair(MAGIC, MAGIC));
}

template <> decrementable_end_tester<multimap<int,int> >::decrementable_end_tester () {
    container.insert(make_pair(MAGIC, MAGIC));
}

int main () {
    // forward_list, unordered_([multi](set|map)) don't use bidirectional iterators.
#if HAVE_ARRAY
    decrementable_end_tester<array<int,1> >().test();
#endif
    decrementable_end_tester<vector<int> >().test();
    decrementable_end_tester<deque<int> >().test();
    decrementable_end_tester<list<int> >().test();
    decrementable_end_tester<set<int> >().test();
    decrementable_end_tester<multiset<int> >().test();
    decrementable_end_tester<map<int,int> >().test();
    decrementable_end_tester<multimap<int,int> >().test();
#if HAVE_SPAN
    decrementable_end_tester<span<int,1> >().test();
#endif
}

Should run without tripping any assertions.


I hope that was helpful. Pretty much all of that was me working to convince myself that end() - 1 was indeed valid

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM