Topic: Using operator-> with new iterator concepts
Author: Richard Smith <richard@ex-parrot.com>
Date: Mon, 11 May 2009 00:49:12 CST Raw View
I've been looking through the new iterator concepts in the current C+
+0x working paper [n2857] and am slightly worried that, if I'm
understanding things correctly, operator-> is not terribly useful on
the new iterator concepts.
As an example, let's consider a simple C++03 template that calls uses
operator->:
template <class InputIterator>
void algo( InputIterator first, InputIterator last ) {
for ( ; first != last; ++first )
first->fn();
}
Let's try to convert this to use C++0x's concepts:
concept Foo<typename T> {
void fn( const T& );
};
template <std::InputIterator Iterator>
requires Foo<Iterator::value_type>
void algo( Iterator first, Iterator last ) {
for ( ; first != last; ++first )
first->fn();
}
Whilst this accurately reflects what we think the requirements should
be, it isn't good enough. The InputIterator concept defines
[24.2.2/1]:
typename pointer = typename X::pointer;
requires Convertible<pointer, const value_type*>;
pointer operator->( const X& );
And the meaning of an expression x->m in a constrained template is
defined by [13.5.6/2]:
| If declared in a concept or concept map, operator-> shall be a
| non-member associated function taking exactly one parameter.
| It implements class member access using ->
|
| postfix-expression -> id-expression
|
| An expression x->m is interpreted as (operator->(x))->m for an
| object x of type T if operator->(T) exists and if the operator is
| selected as the best match function by the overload resolution
| mechanism (13.3).
So within the constrained template we're interested in the expression
first->fn. The type of 'first' is an archetype of InputIterator and
so operator->(Iterator) exists and returns an InputIterator::pointer.
But pointer isn't a raw pointer: it's merely convertible to one, so
the outer -> in the expression (operator->(first))->fn makes no sense.
Adding an additional requirement to our template would seem to fix
this:
template <std::InputIterator Iterator>
requires Foo<Iterator::value_type>
&& SameType<const Iterator::value_type*, Iterator::pointer>
void algo( Iterator first, Iterator last );
And now, as I understand it, the template should compile. But let's
try instantiating it:
struct foo { void fn() const; };
std::vector<foo> foos = get_foos();
algo( foos.begin(), foos.end() );
The thing to notice here is that we are passing algo() a pair of (non-
const_)iterators, and the pointer type of a (non-const_)iterator is
value_type*, not const value_type*. Although I haven't yet managed to
find the exact wording in clause 23, I'm something has to define a
RandomAccessIterator concept_map for std::vector<T,A>::iterator and
this cannot possibly define pointer as anything other than T*. This
means that the requirement clause doesn't hold for the (non-const_)
iterator, and so no overload of algo() will be found.
Replace begin() and end() with cbegin() and cend() and the code will
work fine.
But this strikes me as a fairly big problem. It means we cannot write
constrained-template algorithms that use operator-> and that will work
on both const_iterators and (non-const_)iterators.
The problems goes deeper. In the current C++03 standard, it's quite
common to have iterators that have operator* return by value. In such
cases, operator-> is conventionally defined to return a proxy pointer
type:
typedef value_type reference;
reference operator*() const { /* ... */ }
struct pointer {
pointer( value_type v ) : v(v) {}
value_type* operator->() const { return &v; }
operator value_type*() const { return &v; }
private:
value_type v;
};
pointer operator->() const { return **this; }
In our unconstrained C++0x code, we can use such an iterator without
problem; and such an iterator conforms to the various C++0x iterator
concepts as pointer is convertible to value_type const* and reference
to value_type const&. However, in my constrained template algo(), we
cannot use this because pointer is not a raw pointer.
The latter case -- of supporting iterators that return by value and
use proxy pointers -- is probably salvageable. We could introduce a
helper concept:
auto concept IteratorProxyPointer<typename P, typename V> {
V const* operator->(P const&);
};
and then add a requirement to the InputIterator concept:
requires IteratorProxyPointer<pointer, value_type>;
Under the current wording of the working draft, I think this probably
needs a explicit concept_map for the normal case of a non-proxy
pointer:
template <typename T>
concept_map IteratorProxyPointer<T const*, T> {
T const* operator->(T const* p) { return p; }
};
It's not completely clear that this is needed because the paragraph
describing how associated functions requirements are satisified
[14.10.2.1/4] doesn't really cope with the case of operator-> (or
operator[]) because of it (they) are not ordinary binary infix
operators.
With this helper concept in place, I think this fixes the proxy case,
but it does nothing for earlier the const/non-const issues. Nor does
rewriting the code to write (*i).m instead of i->m help as precisely
the same issues exist regarding reference's convertibility to const
value_type&.
Any thoughts?
Richard Smith
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Greg Herlihy <greghe@mac.com>
Date: Mon, 11 May 2009 13:21:10 CST Raw View
On May 10, 11:49 pm, Richard Smith <rich...@ex-parrot.com> wrote:
> And the meaning of an expression x->m in a constrained template is
> defined by [13.5.6/2]:
>
> | If declared in a concept or concept map, operator-> shall be a
> | non-member associated function taking exactly one parameter.
> | It implements class member access using ->
> |
> | postfix-expression -> id-expression
> |
> | An expression x->m is interpreted as (operator->(x))->m for an
> | object x of type T if operator->(T) exists and if the operator is
> | selected as the best match function by the overload resolution
> | mechanism (13.3).
>
> So within the constrained template we're interested in the expression
> first->fn. The type of 'first' is an archetype of InputIterator and
> so operator->(Iterator) exists and returns an InputIterator::pointer.
> But pointer isn't a raw pointer: it's merely convertible to one, so
> the outer -> in the expression (operator->(first))->fn makes no sense.
The outer -> makes perfect sense - because even if the first call to
operator->() does not return a pointer - then the compiler invokes the
-> operator on the object that is returned - and once again on the
object that that object returns. In fact, the compiler will keep
invoking the operator -> recursively on each successive object
returned - until the compiler obtains a pointer to the expected object
(the one that has the specified member being accessed).
Greg
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Greg Herlihy <greghe@mac.com>
Date: Mon, 11 May 2009 22:49:20 CST Raw View
On May 10, 11:49 pm, Richard Smith <rich...@ex-parrot.com> wrote:
>
> template <std::InputIterator Iterator>
> requires Foo<Iterator::value_type>
> && SameType<const Iterator::value_type*, Iterator::pointer>
> void algo( Iterator first, Iterator last );
>
> And now, as I understand it, the template should compile.
The above function template will work only with Iterators whose
value_types are const (that is, for const_iterator types). Otherwise,
(for ordinary iterator types) the SameType comparison will fail (due
to left-hand type's added const qualification). Therefore, to have algo
() support both const_ and non-const_iterators, all that is needed is
to remove the const qualification from "value_type".
> try instantiating it:
>
> struct foo { void fn() const; };
> std::vector<foo> foos = get_foos();
> algo( foos.begin(), foos.end() );
>
> The thing to notice here is that we are passing algo() a pair of (non-
> const_)iterators, and the pointer type of a (non-const_)iterator is
> value_type*, not const value_type*.
The pointer type of a const_iterator is also value_type* (not const
value_type*) because value_type will incorporate any necessary const
or volatile qualification.
> But this strikes me as a fairly big problem. It means we cannot write
> constrained-template algorithms that use operator-> and that will work
> on both const_iterators and (non-const_)iterators.
In fact, it takes more work to write a constraint that distinguishes
between const_ and non_const iterators than it is to write one that
treats the two indiscriminately.
> The problems goes deeper. In the current C++03 standard, it's quite
> common to have iterators that have operator* return by value. In such
> cases, operator-> is conventionally defined to return a proxy pointer
> type:
>
> typedef value_type reference;
> reference operator*() const { /* ... */ }
> struct pointer {
> pointer( value_type v ) : v(v) {}
> value_type* operator->() const { return &v; }
> operator value_type*() const { return &v; }
> private:
> value_type v;
> };
> pointer operator->() const { return **this; }
>
> In our unconstrained C++0x code, we can use such an iterator without
> problem; and such an iterator conforms to the various C++0x iterator
> concepts as pointer is convertible to value_type const* and reference
> to value_type const&. However, in my constrained template algo(), we
> cannot use this because pointer is not a raw pointer.
As I pointed out in an earlier post, invoking operator->() on an
object eventually has to return a real pointer to something.
Therefore, applying the * operator to this returned pointer type -
will work as expected.
Greg
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Richard Smith <richard@ex-parrot.com>
Date: Tue, 12 May 2009 10:08:03 CST Raw View
On May 11, 8:21 pm, Greg Herlihy <gre...@mac.com> wrote:
> On May 10, 11:49 pm, Richard Smith <rich...@ex-parrot.com> wrote:
>
> > And the meaning of an expression x->m in a constrained template is
> > defined by [13.5.6/2]:
>
> > | If declared in a concept or concept map, operator-> shall be a
> > | non-member associated function taking exactly one parameter.
> > | It implements class member access using ->
> > |
> > | postfix-expression -> id-expression
> > |
> > | An expression x->m is interpreted as (operator->(x))->m for an
> > | object x of type T if operator->(T) exists and if the operator is
> > | selected as the best match function by the overload resolution
> > | mechanism (13.3).
>
> > So within the constrained template we're interested in the expression
> > first->fn. The type of 'first' is an archetype of InputIterator and
> > so operator->(Iterator) exists and returns an InputIterator::pointer.
> > But pointer isn't a raw pointer: it's merely convertible to one, so
> > the outer -> in the expression (operator->(first))->fn makes no sense.
>
> The outer -> makes perfect sense - because even if the first call to
> operator->() does not return a pointer - then the compiler invokes the
> -> operator on the object that is returned
Yes, I'm aware of the recursive nature of operator->. In this case it
tries to invoke operator-> again on the return type which is an
archetype of Iterator::pointer. But it can't as there is no operator-
> that takes the Iterator::pointer archetype. So perhaps I shouldn't
have said it makes no sense, but it doesn't compile either.
In a unconstrained template this would be fine because
Iterator::pointer would almost certainly turn out to be a raw pointer
(or if not, to have an operator->) but the archetype is not a raw
pointer and does not have an operator-> and so, in a constrained
template, the code does not compile.
In any case, this point was incidental to my argument.
Richard
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]
Author: Richard Smith <richard@ex-parrot.com>
Date: Tue, 12 May 2009 12:46:19 CST Raw View
On May 12, 5:49 am, Greg Herlihy <gre...@mac.com> wrote:
> On May 10, 11:49 pm, Richard Smith <rich...@ex-parrot.com> wrote:
>
> > try instantiating it:
>
> > struct foo { void fn() const; };
> > std::vector<foo> foos = get_foos();
> > algo( foos.begin(), foos.end() );
>
> > The thing to notice here is that we are passing algo() a pair of (non-
> > const_)iterators, and the pointer type of a (non-const_)iterator is
> > value_type*, not const value_type*.
>
> The pointer type of a const_iterator is also value_type* (not const
> value_type*) because value_type will incorporate any necessary const
> or volatile qualification.
That might be a good solution to this problem, but it is not what the
working draft [N2857] currently says. For example, in 24.2.6/11 it
defines a concept map for raw pointers:
template<ObjectType T>
concept_map RandomAccessIterator<const T*> {
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef const T& reference;
typedef const T* pointer;
}
If what you say is true, the latter should have a const-qualified
value_type, which it doesn't. Similarly in 23.2.6, the Container
concept has (truncating heavily):
concept Container<typename C> {
ObjectType value_type = typename C::value_type;
requires SameType<ForwardIterator<iterator>::value_type,
value_type>
&& SameType<ForwardIterator<const_iterator>::value_type,
value_type>;
}
So here a Container whose const_iterator::value_type is const-
qualified will fail to match the Container concept. I could have
believed one of these might have been a typo; but two seems a bit
improbable.
Can you find any evidence in the working draft that
Iterator::value_type ought to be const-qualified?
I would also be worried that requiring const_iterators to have a const-
qualified value_type would be error-prone with existing code. Without
a concept_map that explicitly does something to contrary, the
concept_map's value_type is taken from iterator, and existing practice
is generally not to const-qualify value_type on a const_iterator.
> > But this strikes me as a fairly big problem. It means we cannot write
> > constrained-template algorithms that use operator-> and that will work
> > on both const_iterators and (non-const_)iterators.
>
> In fact, it takes more work to write a constraint that distinguishes
> between const_ and non_const iterators than it is to write one that
> treats the two indiscriminately.
If it were true, that would be good.
> > The problems goes deeper. In the current C++03 standard, it's quite
> > common to have iterators that have operator* return by value. In such
> > cases, operator-> is conventionally defined to return a proxy pointer
> > type:
>
> > typedef value_type reference;
> > reference operator*() const { /* ... */ }
> > struct pointer {
> > pointer( value_type v ) : v(v) {}
> > value_type* operator->() const { return &v; }
> > operator value_type*() const { return &v; }
> > private:
> > value_type v;
> > };
> > pointer operator->() const { return **this; }
>
> > In our unconstrained C++0x code, we can use such an iterator without
> > problem; and such an iterator conforms to the various C++0x iterator
> > concepts as pointer is convertible to value_type const* and reference
> > to value_type const&. However, in my constrained template algo(), we
> > cannot use this because pointer is not a raw pointer.
>
> As I pointed out in an earlier post, invoking operator->() on an
> object eventually has to return a real pointer to something.
As I pointed out in an earlier post (assuming it finally makes it to
comp.std.c++), it doesn't. When the template is being instantiated,
there is no operator-> that takes an Iterator::pointer archetype and
so it doesn't compile without a SameType requirement to say that
Iterator::pointer is a raw pointer; and with the SameType requirement,
it doesn't instantiate because the proxy pointer type isn't a raw
pointer. Hence the need for some more complicated concept that
supports proxy pointers, like the IteratorProxyPointer I gave.
> Therefore, applying the * operator to this returned pointer type -
> will work as expected.
I assume you're not advocating writing code like (*i.operator->()).m
()?
Actually, (*i).m() has just as many problems as i->m(). Again, you
need a SameType requirement that says something about
Iterator::reference. Either you write
SameType<Iterator::reference, value_type&>
(with or without const-qualfication), in which case the case when
operator* returns by value fails to match it; or you write
SameType<Iterator::reference, value_type>
in which case the usual case of operator* returning by reference fails
to match it. And unlike the case of operator->, you can't use a
helper concept like IteratorProxyPointer to save you because operator*
isn't invoked recursively (and for good reason).
So right at the moment, I'm sticking by all of the points I've made.
Richard Smith
--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]