9

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 to T; if X is a const iterator, reference is a reference to const 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" if a is dereferencable.

from Table 106 (iterators) *r must have type reference where r is of type X& 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 and b are values of type X): If a and b are both dereferenceable, then a == 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.

Community
  • 1
  • 1
rici
  • 201,785
  • 23
  • 193
  • 283

2 Answers2

2

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.

Pete Becker
  • 69,019
  • 6
  • 64
  • 147
  • 1
    I edited the question to say that more clearly; I remember thinking about that objection when I started writing it. My issue is that I don't see how to write a conformant forward iterator; the container issue is more or less a side-effect, which is why the word `iterator` is in the question. – rici Dec 14 '12 at 00:02
2

The best model is std::vector< bool >. It is as close to being compliant as possible, yet its iterators do yield proxies.

The standard even specifies that std::vector<bool>::reference is a class. Yet the container requirements table specifies that X::reference yields "lvalue of T." Thus is it strictly non-compliant.

But the iterators aren't bound to T. The iterator value_type must be T and consulting the input iterator requirements, reference must convert to value_type.

As Pete Becker mentions, the tables of requirements are rather broad blankets, and individual algorithms specify what they need. Only an algorithm that requires reference to really be a reference will break, which is kind of just stating the obvious.

Potatoswatter
  • 126,977
  • 21
  • 238
  • 404
  • Yeah, I've looked at `std::vector`. But if that's the best model possible, that's disappointing. I would have liked to be able to write something that wouldn't be savaged by Herb Sutter. ("If anyone else had written vector, it would have been called 'nonconforming' and 'nonstandard.'") – rici Dec 17 '12 at 06:21
  • OK, I edited the first paragraph of the question in a vague attempt to make it clearer what I'm trying to get an answer to. If you want to look at another (heroic!) attempt to deploy proxy iterators, you might check out http://www.justsoftwaresolutions.co.uk/articles/pair_iterators.pdf – rici Dec 17 '12 at 06:58
  • @rici What wiggle room is there? Either the iterator returns proxies or real references. My opinion is that `vector` is ahead of its time, precisely because it goes beyond obtaining storage and handing back pointers. – Potatoswatter Dec 17 '12 at 06:58
  • So I take it that your view is that "bound to the same object" is to be interpreted in some philosophical, possibly even Platonic sense, rather than meaning that they are references to the *same* object? – rici Dec 17 '12 at 07:04
  • @rici Sorry, I should admit not having fully read the question. You already covered most of this analysis. Yeah, I think the standard has a ways to go if proxy containers are to be true containers and supply true forward iterators (or better). However, it's more paperwork than technological development. The reason `iterator::reference` and `container::reference` are there, in addition to `value_type`, is so they can be nontrivially different. But the role of implicit conversion needs to be clarified. Fully generic algorithms should use `static_cast` to avoid stacking user-defined conversions. – Potatoswatter Dec 17 '12 at 07:11
  • As for interpreting the existing standard verbatim, "bound to the same object" makes sense only if they're references (as they're required to be). `vector` should specify or note that not all requirements are met. But most algorithms are defined in terms of the expressions that use the operands. As to whether passing `std::vector::iterator` to `std::reverse` (which requires bidirectional iterators, §25.1/5) is UB, yes, "but" the hack is probably good enough. – Potatoswatter Dec 17 '12 at 07:19