Arithmetic on end() iterator

[亡魂溺海] 提交于 2021-01-26 09:15:35

问题


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.


回答1:


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.




回答2:


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 thatsizeof(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.




回答3:


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



来源:https://stackoverflow.com/questions/25928547/arithmetic-on-end-iterator

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!