I have more or less come to the conclusion that it is impossible to write a conformant container whose value_type was not stored directly in the container. I think this is unfortunate, because I frequently end up wishing I had containers where the value type is either partially computed or assembled from discontiguous pieces (examples below, but not directly relevant to the question). I know how to write iterators which use proxy objects, although it's pretty annoying. But I'm now wondering whether there really is space in the C++ standard for such beasts. There's probably too much verbiage here; the tl;dr version is simple: What do paragraphs 1 and 6 of §24.2.5 really mean, and to what extent will violating the apparent meaning break standard algorithms? Or, to put it another way, how can they be interpreted to allow proxy iterators?
As Pete Becker points out, there is really nothing forcing my containers to conform to requirements set out for standard library containers. But in order to use a container with many standard algorithms, it must either have a conformant iterator with at least a forward_iterator_tag
, or it must lie about that but still manage to satisfy the operational (if not formal) requirements the particular algorithm imposes on its iterators.
Here is my reasoning:
Table 96 (§ 23.2.1), container requirements, includes:
Expression Return type Assertion/note
------------ ------------- ---------------------
X::iterator iterator type any iterator category
whose value that meets the
type is T forward iterator
requirements.
Convertible to
const_iterator.
a.begin() iterator;
const_iterator for
constant a.
Now, forward iterator:
§ 24.2.5, para. 1:
A class or pointer type
X
satisfies the requirements of a forward iterator if …— if
X
is a mutable iterator,reference
is a reference toT
; ifX
is a const iterator,reference
is a reference toconst T
It's true that there is no direct requirement for *a
to return reference
(where a
is of type X
). The requirements are:
from Table 107 (input iterators)
*a
must be "convertible to T" ifa
is dereferencable.from Table 106 (iterators)
*r
must have typereference
wherer
is of typeX&
and is dereferencable.
However, Table 106 also specifies that ++r
returns X&
, so *++r
must be reference
. Also, (as per Table 107), *a++
must be reference
, although (Table 109) a[n]
only needs to be "convertible to reference". I've got to say that I don't see how *a
where a
is of type X
and *r
where r
is of type X&
could be different, but maybe I'm missing some subtlety.
Maybe there is a little wiggle-room here, but not much; at some point, you need to be prepared to create a T
, if you don't actually have one in the container, so that you can provide a reference to it.
But the kicker is
§ 24.2.5, para. 6 (
a
andb
are values of typeX
): Ifa
andb
are both dereferenceable, thena == b
if and only if*a
and*b
are bound to the same object.
I can't find a formal definition of bound to
, but it seems to me that the usual strategy for making iterators of non-addressable objects is to create a proxy object, generally stored inside the iterator itself. In this case, it would require an extremely generous understanding of what "bound to" means to interpret 24.2.5/6 in any way other than to forbid equality comparisons succeeding between two different iterator objects, even if they are logically indicating the same position in the container.
On the other hand, I note that Dietmar Kühl, who ought to know, in his response to this question says that:
C++ 2011 got relaxed requirements and iterators aren't necessarily required to yield an lvalue
So, can an iterator return a proxy, or can it not? If it can, what is the nature of such a proxy? Where does my reasoning that such an iterator is non-conformant fail?
As promised, a few containers whose effective value_types would not be stored contiguously in the container:
1) A compact associative container whose key and value types can be more efficiently stored in two separate vectors. (Keeping the keys in a vector can also improve cache-friendliness, as well as reducing allocation overhead.)
2) A vector<T>
which masquerades as a map<integer_type, T>
, simplifying inter-operability with other map<X, T>
types.
3) A logical container formed by zipping several other containers, producing a logical value_type which is a tuple
of references to the value types of the zipped containers. (In some applications, one or more of the zipped containers might be wholly computed, either as a function of the other values, or as a sequence number.)
4) A view of a container of an aggregate type which has only some of the values. (Quite possibly, both the underlying container and the view are tuples where the view tuple's type list is a subset, possibly in a different order, of the underlying containers' types).
I'm sure other people could easily add to this list; these are just the ones I've hacked around in some way or another in the past couple of months.
struct vec{ iterator begin() ; const_iterator begin() const; };
The primary purpose of an iterator is to allow a user to process every element of a container while isolating the user from the internal structure of the container. This allows the container to store elements in any manner it wishes while allowing the user to treat it as if it were a simple sequence or list.
An iterator is an object (like a pointer) that points to an element inside the container. We can use iterators to move through the contents of the container. They can be visualised as something similar to a pointer pointing to some location and we can access content at that particular location using them.
Don't limit yourself by thinking about a "conformant container": there is nothing in the standard that depends on having one. Think of container requirements as a shorthand way of describing the requirements for the containers that are defined in the standard. Nothing more. As long as the iterators that your container produces are valid, you're fine with all the corresponding algorithms, and, presumably, with algorithms that you write yourself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With