Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Decrementable requirements of general end iterators vs container `end()`

I am working on a ReversibleContainer and its associated LegacyRandomAccessIterators. They wrap a pre-existing data structure that represents a directly-indexable collection of objects. I decided to allow the iterators to exist standalone, and to make default-constructed iterators represent the "end":

// constructs an iterator that provides a view of 'data'
the_iterator (thedata *data, difference_type index = 0);

// constructs an iterator representing the end
the_iterator ();

So I can do e.g.:

std::for_each(the_iterator(data), the_iterator(), ...);

The iterators do all the work. The container is pretty lightweight. I implemented the container's begin() and end() like this:

struct the_container {
    the_data *data; // <- object wrapped by this container
    the_iterator begin () { return the_iterator(data); }
    the_iterator end () { return the_iterator(); }
};

I've got it working just fine, but in testing I realized that I messed up and it does not meet the basic Container requirements because:

  • As it turns out, for containers with bidirectional iterators, end() is required to return a decrementable iterator when the container is non-empty, but
  • My default-constructed end iterator doesn't store any information about any thedata, and so it can't be decremented because it isn't aware of what the "last element" of any particular collection is.

So I've got to fix the container now. The solution I'm thinking of is (let data->number_of_items contain the item count):

  • Continue to allow default-constructed iterators to represent the "end".
  • Also let the_iterator(data, data->number_of_items) represent the "end", in line with container requirements. This iterator would be decrementable.

Then the container would do:

struct the_container {
    the_data *data; // <- object wrapped by this container
    the_iterator begin () { return the_iterator(data, 0); }
    the_iterator end () { return the_iterator(data, data->number_of_items); }
};

Now, that's fine, and it satisfies all the Container requirements. However, I'm now left wondering if my default-constructed iterators are allowed to exist at all.

So, my question, then, is: While Container puts decrementability requirements on the iterator that it returns from end(), are there similar requirements for iterators that just represent the "end" of some data but aren't involved with the container's end()?

More formally, if:

  • j is a bidirectional iterator
  • container.empty() == false
  • ( j == container.end() ) == true

Then does --j need to be valid and need to end up pointing to the last element of the container? An example of that situation in my case is:

the_container container(data); // <- assume data->number_of_items > 0
the_iterator b = container.begin();
the_iterator e = container.end();
the_iterator j;

assert(container.empty() == false);
assert(e == j);
assert(distance(b, e) == distance(b, j));

-- e;  // <- this is required to be well-defined
-- j;  // <- but is this??

So, yeah, that's my question. I'm worried that maybe some implementation of something or other in e.g. <algorithm> might assume that one of my "end" iterators is decrementable, or that I'm breaking something subtle that I don't understand.

like image 752
Jason C Avatar asked Sep 06 '25 15:09

Jason C


1 Answers

As a frame challenge, what you have here is a sentinal. A sentinal is something you can compare iterators to, but is not an iterator.

The motivating example is a type that when compared with a char pointer just checks if the dereference of the char pointer is null. You can use this to write C-level "read string until null" while still using for(:) loops and the like.

The ranged based for(:) loop was augmented to allow begin and end to have distinct types to allow sentinals to exist and be maximally efficient.

Consider replacing your default constructed iterator trick with an end sentinal. This doesn't work with many pre-ranges-v2 algorithms, but at least you are uwing tried and true patterns.

And no, I don't think your end iterator violates the rules. As an example, iterators from two containers can have non sense == results under the standard. Your end iterator can be treated as an iterator from another container with a predictable == behaviour.

Now, what happens when you pass it to an algorithm? That probably makes your program ill-formed; the algorithm can detect what guarantees you give with traits, and your fake end iterator is not a valid bidirectional iterator into the same container as the begin iterator.

like image 95
Yakk - Adam Nevraumont Avatar answered Sep 09 '25 10:09

Yakk - Adam Nevraumont