C++0x concepts and ref qualifiers inconsistency

107 views
Skip to first unread message

SG

unread,
Jan 7, 2009, 1:15:43 PM1/7/09
to
Hello!

Provided I fully understand the relevent parts of the document N2800,
I found a slight but yet surprizing inconsistency w.r.t. the meaning
of ref qualifiers (or lack thereof) for member functions. I'm
basically asking for comments on whether I got that right and if yes
I'd like to hear opinions on this issue and the proposed change of
"concept rules" I'm going to mention at the end.

According to 14.9.2.1 [concept.map.fct] a member function requirement
of the form
void T::foo();
is equal to
void T::foo() &;
since both versions make 'x' in the expression 'E' to be an lvalue
(see 14.9.2.1/4). This function requirement is satisfied when the
expression 'E' is well-formed. So, the following example should
compile:

struct A {
void foo() &;
};

concept HasFoo<typename T> {
void foo();
}

concept_map HasFoo<A> {} // OK

IMHO this is counter-intuitive because the lack of a ref qualifier in
plain member function declarations implies that the function can be
invoked on both, rvalues and lvalues. But the lack of a ref qualifier
in member function requirements doesn't imply that this member
function can be invoked on rvalues. Also, this makes the lvalue ref
qualifier totally useless in member function requirements. I stumbled
upon this because I was trying to understand what the concept
HasAssign<> actually means ...

auto concept HasAssign<typename T, typename U> {
typename result_type;
result_type T::operator=(U);
}

... and how built-in types are supposed to satisfy this when an lvalue
ref qualifier is missing. In constrained templates I expected to be
able to assign to rvalues as well because we're used to be able to
call member functions lacking ref qualifiers on both, lvalues and
rvalues. But then I noticed the actual proposed satisfaction rules.

To make this a bit more consistent and more intuitive I thought of
proposing to change the function requirement satisfaction rules a
little bit. (See 14.9.2.1/4) The idea is to render a member function
requirement (lacking any ref qualifiers) satisfied only if both
versions of the expression 'E' (one version with 'x' being an lvalue
and one version with 'x' being an rvalue) are well-formed. This would
NOT make the language less expressive but only more consistent and
more intuitive. To retain the semantics of the current concepts
defined in N2800 we would simply need to add an lvalue ref qualifier
for every member function requirement that currently lacks ref
qualifiers -- for example HasAssign:

auto concept HasAssign<typename T, typename U> {
typename result_type;
result_type T::operator=(U) &; // <- ref qualifier added
}

As mentioned earlier: under current rules both concept versions seem
to be equivalent.


Cheers!
SG

--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use
mailto:std...@netlab.cs.rpi.edu<std-c%2B%2...@netlab.cs.rpi.edu>
]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html ]

Douglas Gregor

unread,
Jan 11, 2009, 4:06:42 AM1/11/09
to
On Jan 7, 10:15 am, SG <s.gesem...@gmail.com> wrote:
> According to 14.9.2.1 [concept.map.fct] a member function requirement
> of the form
> void T::foo();
> is equal to
> void T::foo() &;
> since both versions make 'x' in the expression 'E' to be an lvalue
> (see 14.9.2.1/4). This function requirement is satisfied when the
> expression 'E' is well-formed. So, the following example should
> compile:
>
> struct A {
> void foo() &;
> };
>
> concept HasFoo<typename T> {
> void foo();
> }
>
> concept_map HasFoo<A> {} // OK
>
> IMHO this is counter-intuitive because the lack of a ref qualifier in
> plain member function declarations implies that the function can be
> invoked on both, rvalues and lvalues. But the lack of a ref qualifier
> in member function requirements doesn't imply that this member
> function can be invoked on rvalues.

Good point. That's a problem.

> I stumbled
> upon this because I was trying to understand what the concept
> HasAssign<> actually means ...
>
> auto concept HasAssign<typename T, typename U> {
> typename result_type;
> result_type T::operator=(U);
> }
>
> ... and how built-in types are supposed to satisfy this when an lvalue
> ref qualifier is missing. In constrained templates I expected to be
> able to assign to rvalues as well because we're used to be able to
> call member functions lacking ref qualifiers on both, lvalues and
> rvalues. But then I noticed the actual proposed satisfaction rules.

Right.

> To make this a bit more consistent and more intuitive I thought of
> proposing to change the function requirement satisfaction rules a
> little bit. (See 14.9.2.1/4) The idea is to render a member function
> requirement (lacking any ref qualifiers) satisfied only if both
> versions of the expression 'E' (one version with 'x' being an lvalue
> and one version with 'x' being an rvalue) are well-formed.

In addition to type-checking E with 'x' as both an lvalue and as an
rvalue, we need to place some consistency conditions on the member
functions we find, and pick which member function will be the seed of
the associated function candidate set. For example, we could have some
wacky class 'X':

class X {
public:
int foo() &;
float foo() &&;
};

concept Foo<typename T> {
typename result_type;
result_type T::foo();
}

concept_map Foo<X> { }; // requirement for X::foo is satisfied by
two different functions... with different return types!

I don't have a good answer for this. We probably want to reject this
concept map because the deduction for result_type is inconsistent.
However, the user could specify the result type:

concept_map Foo<X> { typedef int result_type; } // okay

Perhaps the easiest way to express consistent semantics is for the
member requirement

result_type T::foo();

to be equivalent to:

result_type T::foo() &;
result_type T::foo() &&;

That captures the notion that lvalue and rvalue implicit object
arguments can end up calling different member functions (or not)
relatively easily.

> This would
> NOT make the language less expressive but only more consistent and
> more intuitive.

Well, it certainly making it more consistent. I can't say that being
able to assign to a class rvalue is ever "intuitive" :)

> To retain the semantics of the current concepts
> defined in N2800 we would simply need to add an lvalue ref qualifier
> for every member function requirement that currently lacks ref
> qualifiers -- for example HasAssign:
>
> auto concept HasAssign<typename T, typename U> {
> typename result_type;
> result_type T::operator=(U) &; // <- ref qualifier added
> }

I find it a bit unfortunate that users will have to delve into ref-
qualifiers (and probably understand rvalue references) to write a
simple Assignable concept that works properly with built-in types.
That said, it's probably better than having the current hole in the
type system.

We should open an issue for this and come up with some proposed
wording.

- Doug


--
[ comp.std.c++ is moderated. To submit articles, try just posting with ]

[ your news-reader. If that fails, use mailto:std...@netlab.cs.rpi.edu]

SG

unread,
Jan 13, 2009, 11:57:45 AM1/13/09
to
Hello, Doug! Thanks for taking the time to respond.

On 11 Jan., 10:06, Douglas Gregor <doug.gre...@gmail.com> wrote:
> On Jan 7, 10:15 am, SG <s.gesem...@gmail.com> wrote:

> > [...]


> Good point. That's a problem.

> [...]


> In addition to type-checking E with 'x' as both an lvalue and as an
> rvalue, we need to place some consistency conditions on the member
> functions we find, and pick which member function will be the seed of
> the associated function candidate set. For example, we could have some
> wacky class 'X':
>
> class X {
> public:
> int foo() &;
> float foo() &&;
> };
>
> concept Foo<typename T> {
> typename result_type;
> result_type T::foo();
> }
>
> concept_map Foo<X> { }; // requirement for X::foo is satisfied by
> two different functions... with different return types!

Oh. I didn't think of that.

> [...]


> Perhaps the easiest way to express consistent semantics is for the
> member requirement
>
> result_type T::foo();
>
> to be equivalent to:
>
> result_type T::foo() &;
> result_type T::foo() &&;

Sounds like a good idea.

> That captures the notion that lvalue and rvalue implicit object
> arguments can end up calling different member functions (or not)
> relatively easily.

The possibility to call different functions is given anyways because
of the candidate sets, right? To me it looks like overload resolution
is donce twice. The first time when a function requirement overload
is selected before instantiation and the 2nd time when one of the
functions of the requirement's canditate set is chosen while
instantiatiing templates. Is this correct?

[...]


> > auto concept HasAssign<typename T, typename U> {
> > typename result_type;

> > result_type T::operator=(U) &; // <- ref qualifier added
> > }
>
> I find it a bit unfortunate that users will have to delve into ref-
> qualifiers (and probably understand rvalue references) to write a
> simple Assignable concept that works properly with built-in types.
> That said, it's probably better than having the current hole in the
> type system.

So, it DOES constitute a hole? I wasn't sure because I still struggle
to understand the intended meaning of the function requirement's
pseudo signatures and part 14.10 of the draft. It is covered in two
different parts as far as I can tell:

(1) 14.9 for "concept_map checking".
(2) 14.10 for "type checking in restricted templates".

I have to admit that I currently understand very little of 14.10. I'm
assuming that the rules of 14.9 and 14.10 are supposed to avoid the
delay of any possible type error until template instantiation. In
that case the type checking rules in 14.9 would imply most of the
rules in 14.10 and vice versa.

I'm not yet in a position to determine whether there are any "holes".
But I currently think that this rvalue/lvalue issue also applies to
function parameters. Example:

concept HasFoo<typename T> {
void foo(T); // What does this pseudo signature mean?
}

According to 14.9.2.1 it means the expression "foo(x)" where x is an
*rvalue* of type T is well-formed. There's no word on lvalue
parameters in this case. So, the following should be well-formed:

struct S {};

void foo(const S&) = delete;
void foo(const S&&);

concept_map HasFoo<S> {}

If I remember correctly from various concept examples it seems that
within a restricted template the type "T" of a pseudo signature
behaves like a const reference to T. Assuming that this is true, the
following would be well-formed as well:

template<typename T>
requires HasFoo<T>
void bar(const T& x) {
foo(x);
}

But clearly 'x' is an lvalue and 'foo' taking an lvalue of type S is
deleted. So this would be an error that is delayed until template
instantiation. Am I correct?

Cheers!
SG

Reply all
Reply to author
Forward
0 new messages