问题
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
ande -= n
for any integer n in [ 0, container.size() ]e + n
ande += 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:
- Container: The "
end()
returns one past the end of a" requirement mentioned above. - RandomAccessIterator:
i - n
, defined in terms of-=
, no constraints given. - RandomAccessIterator:
r -= n
, defined in terms of+=
, no constraints given. - RandomAccessIterator:
r += n
, defined in terms of--r
forn < 0
, no constraints given. - BidirectionalIterator:
--a
- Precondition: a is decrementable → there exists b such that
a == ++b
. - Postcondition: a is dereferenceable.
- Postcondition:
--(++a) == a
- Postcondition: if
--a == --b
thena == b
- Postcondition:
a
and--a
are the same iterator instance.
- Precondition: a is decrementable → there exists b such that
- 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)".
- Container: States that
size()
is semantically equivalent tostd::distance(begin(), end())
- distance: Returns the number of increments to get from
first
tolast
.
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
forn <= 0
is valid, from (4).e -= n
forn >= 0
is valid, from (3).e - n
forn >= 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, sincebegin()
is not decrementable (by definition of decrementability) and eventually the iterator must hitbegin()
.
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