overload resolution and conversion ops

X

xtrigger303

Hi to all,
I was reading Mr. Alexandrescu's mojo article and I've a hard time
understanding the following.
Let's suppose I have:

//code
struct A {};

struct B : A {};

struct C
{
operator A() const { return A(); }
operator B() { return B(); }
};

void F( A const & ) {}
void F( B const & ) {}

C const H() { return C(); }

int main()
{
F( C() ); // 1. called with a non-const rvalue
F( H() ); // 2. called with a const rvalue
}
//end code

I understand that if I have a const "C" (rvalue or lvalue, here it
does not matter)
the only option when calling "F" would be to use

C::eek:perator A() const -> F( A const & )


In the case I have a non-const "C" it seems that the compiler
chooses

C::eek:perator B() -> F( B const & )

my understanding of the article is that this is because since
B is derived from A, it's a better match than the base class.

I do not understand then why the constness of operator A() makes
the difference. If I remove it and make it just

C::eek:perator A()

my guess would be that that when calling "F" I would always get a call
to
F( B const & ). But instead the thing does not compile.

My naive understanding then is that the constness of the conversion
operators
AND the fact the B is derived from A all come into play when
"resolving" the call
to "F". The problem is that I cannot fully understand the logic behind
this nor
I can find the right chapter in the standard. Tryed [over.ics.rank].
Any help to understand this better would be greatly appreciated.

Thanks in advance,
Francesco
 
J

James Kanze

I was reading Mr. Alexandrescu's mojo article and I've a hard
time understanding the following.
Let's suppose I have:
//code
struct A {};
struct B : A {};
struct C
{
operator A() const { return A(); }
operator B() { return B(); }
};
void F( A const & ) {}
void F( B const & ) {}
C const H() { return C(); }
int main()
{
F( C() ); // 1. called with a non-const rvalue
F( H() ); // 2. called with a const rvalue}
}
//end code
I understand that if I have a const "C" (rvalue or lvalue,
here it does not matter) the only option when calling "F"
would be to use
C::eek:perator A() const -> F( A const & )

Correct. The C::eek:perator B() conversion operator cannot be
called (is not a viable function, in the words of the standard).
In the case I have a non-const "C" it seems that the compiler
chooses
C::eek:perator B() -> F( B const & )
my understanding of the article is that this is because since
B is derived from A, it's a better match than the base class.
I do not understand then why the constness of operator A()
makes the difference.

It's rather the non-const-ness of operator B() that makes the
difference. Because the operator is not const, it cannot be
called on a const object.
If I remove it and make it just
C::eek:perator A()
my guess would be that that when calling "F" I would always
get a call to
F( B const & ). But instead the thing does not compile.

If the object is const, only const functions can be called on
it. If you remove const everywhere, then no functions can be
called on the object. Whence your error.
My naive understanding then is that the constness of the
conversion operators AND the fact the B is derived from A all
come into play when "resolving" the call to "F".

Sort of. Overload resolution doesn't consider functions which
cannot be called because of a mismatch in the signatures. (It
doesn't take things like access protection into consideration,
so overload resolution can resolve to a private function which
cannot be called.)
The problem is that I cannot fully understand the logic behind
this nor I can find the right chapter in the standard. Tryed
[over.ics.rank]. Any help to understand this better would be
greatly appreciated.

You're not looking far enough. In the terms of the standard,
overload resolution takes place in three steps: creating a list
of candidate functions (results of name lookup and possibly
template instantiations), creating a set of viable functions
from the candidate functions (only those candidate functions
whose signatures allow them to be called), and finally, choosing
the best candidate function. In more usual terminology, only
this last step would be considered "resolution", the first two
steps only construct the set over which resolution is applied.
But regardless of how they are called, all three steps take
place, and the compiler will not consider a function that it
doesn't find during name lookup (first step) or whose signature
will not allow it to be called (second step).
 
J

James Kanze

* James Kanze:
But here you have overlooked the call 'F( C() )'.

Not entirely, but I was mainly concerned with the call
generating the error.
Remove the call 'F(H())' and the const on op B still makes a
difference... :)

Well, it will certainly make a difference in some cases. The
const-ness is part of the type, and will always play some role
in overload resolution. As you say, I was mainly concerned with
the F(H()).
OK, I don't understand this, even after scrutinizing the
standard, but I recognize that your explanation isn't it, that
it ignores the really relevant aspects.

I know that as well. Overload resolution is one of the most
complicated subjects in the standard, taking a full 15 pages of
text, all of it relevant.
I should understand it, after all it's basic!

Basically incomprehensible, you mean.

It's saving grace is that it ends up doing what you intuitively
would want, or generating an error (which can be eliminated by
an explicit cast), so you can write robust C++ without
understanding it.
, and I think I did understand it once, as one of the
volunteers helping Andrei with quality assurance on Mojo. But
darned if I now can get my head around this (same predicament
as the OP)!
Again, here you have overlooked the call 'F( C() )'.

Obviously, I was talking about the case of a const C. But
you're right, I should have made that clearer.
Here's a FLAWED analysis, with some facts. Would appreciate
it if you could return my help in pointing out flaw in your
above explanation, by pointing out flaw(s) in this... :) :)

Well, I can try, a little, but quite frankly, overload
resolution is too complicated for me. I just keep it simple,
and trust the compiler.

In well designed code, it shouldn't matter which function is
called. If it matters, they shouldn't be overloaded. But you
can't always avoid the problem with constructors (since you
can't give them different names), and the problem can definitly
occur when software evolves, but must also retain some backwards
compatibility. And of course, there is still the problem of a
call being ambiguous.
The facts seem to stand.
can be resolved as
F( C().operator A() ) // calls F( A const& )

F( C().operator B() ) // calls F( B const& )
Data point matrix (apparently these are facts):
case const on op A const on op B result
0 no no Ambigious, F(A) or F(B)
1 no YES Ambigious, F(A) or F(B)
2 YES no F(B)
3 YES YES Ambigious, F(A) or F(B)

That's what g++ (the only compiler I have available here)
says. The issues are complicated enough that I wouldn't
count on it being right. (Comeau, or another compiler using
the EDG front-end, has the best chance of being right, since
one of the authors of the EDG front-end is also the author
of section 13 in the standard. But even then, the issues
are complicated enough that a small slip up cannot be
excluded.)
The most interesting case seems to be case 1. Here, from naïve point
of view, the class derivation relationship wants F(B), most specific,
but the extra const "conversion" wants F(A), fewest "conversions".
However, let's first consider cases 0 and 3, where op A() and op B()
compete directly, on equal footing, so to speak.
Hm, standard, what do you say?
OK, §13.3.3.2/2 is the top-level of how to rank implicit conversion
sequences, paraphrased:
Best: Standard conversion sequence
So-so: User defined conversion sequence
Worst: Ellipsis conversion sequence (a "..." formal argument)
What we have is two user defined conversion sequences to be
compared (from C to A or B result), so that top level ranking
doesn't come into play directly, but it can affect rankings
for sub-sequences. For a user-defined conversion sequence is
defined by §13.3.3.1.2 to consist of a standard conversion
sequence followed by a (single) user defined conversion
followed by a standard conversion sequence. Sort of like
pre-conversion adjustment, real conversion, and post fixups.
Case 0
Seq Pre Real (user) Post Calls
s1 identity op A() A -> A const& F(A const&)
s2 identity op B() B -> A const& F(A const&)
s3 identity op B() B -> B const& F(B const&)
s2 is ranked lower than s3 because the post-conversion of s3
(recall, the post conversion is a standard conversion
sequence, not involving any calls) is more specific, at least
according to my reading of §13.3.3.2/4. So that leaves s1 and
s3 to choose from.

-- Standard conversion sequence S1 is a better
conversion sequence than standard conversion
sequence S2 if

-- S1 is a proper subsequence of S2 (comparing the
conversion sequences in the canonical form
defined by 13.3.3.1.1, excluding any Lvalue
Transformation; the identity conversion sequence
is considered to be a subsequence of any
non-identity conversion sequence) or, if not
that,

For once, something rather simple and intuitive: since s1 is
a proper subsequence of s2, s1 is better than s2. (Whatever
else this means, it means that s2 cannot be chosen.)
But s1 and s3 involve the same kinds of conversions.
We do not even need to know the exact rules. The
specificity of the result of s3 does not enter into the
ranking because that specificity is not of a standard
conversion sequence: specificity of class affects only
ranking of standard conversion (like, you have B, then a B
result, doing nothing, is better than direct "upcast" to
A, at least according to my reading of §13.3.3.2/4). So
in this case s1 and s3, as I understand it, ends up with
same rank, and the call is ambigious.

Sounds right to me. You have a choice of calling F(A
const&) with an A, or F(B const&) with a B. Intuitively, if
that isn't ambiguous (supposing that there are no
differences in the way you obtain the A or the B), I don't
know what is.
*A key notion*: that "closeness" in inheritance only
affects ranking of standard conversions, not ranking of a
user-defined conversion.

Yes. A user defined conversion is a user defined
conversion, and there is no ranking between them, ever. All
user defined conversions are equal (with regards to the user
defined conversion---getting the reference to the object may
involve conversions.
Analysis for case 3 would be the same, so, case 1 then:
Case 1
Seq Pre Real (user) Post Calls
s1 identity op A() A -> A const& F(A const&)
s2 C -> C const op B() B -> A const& F(A const&)
s3 C -> C const op B() B -> B const& F(B const&)
Here again s2 is ranked lower than s3. But it would seem
that s1 should prevail, should be ranked higher than s3,
because of the const conversion in the pre-conversion.
However, §13.3.3.2/3 tells us that
"User-defined conversion sequence U1 is a better conversion sequence
than another user-defined conversion sequence U2 if they contain the
same user-defined conversion function or constructor and if the
second standard conversion sequence [the "Post" above] of U1 is
better than the second standard conversion sequence of U2."
And it seems there is no other criterion for top-level
ranking of user defined conversion sequences.

That's the way I read it as well. I'll admit that it's NOT
what I would intuitively expect: I would expect that if
there were two conversion sequences involving user defined
conversions, the pre and the post would be considered. In
this case, that would mean prefering s1. But that doesn't
seem to be the case.
Hence, in order to for s1 to be better than s3 it would
have to involve the same conversion operator, and it would
have to be better in the post-conversion. The
pre-conversion is simply ignored, wrt. ranking. The
result is that s1 and s3 are ranked equally, ambigious.
Finally, analysis for case 2:
Case 2
Seq Pre Real (user) Post Calls
s1 C -> C const op A() A -> A const& F(A const&)
s2 identity op B() B -> A const& F(A const&)
s3 identity op B() B -> B const& F(B const&)
But this analysis fails, in the sense that it tells us
that since s1 and s3 involve different user defined
conversion functions, neither one is ranked better than
the other. So according to this analysis they "should" be
equally ranked, ambigious. But they're not.

At least with the compiler(s) we're using. Your reasoning
seems to hold. (But there *are* 15 pages to be considered,
so the possibility that we've overlooked something can't be
excluded.) I'm having a hard time understanding why this is
different from:

struct C
{
operator std::string const() { return "const" ; }
operator std::string() { return "non-const" ; } ;
} ;

int
main()
{
std::string s1( (C()) ) ;
std::cout << s1 << std::endl ;
return 0 ;
}

In this case, g++ 4.2.1 says ambiguous as well. So maybe
you've just found a bug in g++ (or whatever compiler you're
using).
 
X

xtrigger303

* James Kanze:




Correct.  The C::eek:perator B() conversion operator cannot be
called (is not a viable function, in the words of the standard).
Agreed.
It's rather the non-const-ness of operator B() that makes the
difference.  Because the operator is not const, it cannot be
called on a const object.

But here you have overlooked the call 'F( C() )'.

Remove the call 'F(H())' and the const on op B still makes a
difference... :)

OK, I don't understand this, even after scrutinizing the standard, but
I recognize that your explanation isn't it, that it ignores the really
relevant aspects.  I should understand it, after all it's basic!, and
I think I did understand it once, as one of the volunteers helping
Andrei with quality assurance on Mojo.  But darned if I now can get my
head around this (same predicament as the OP)!
If the object is const, only const functions can be called on
it.  If you remove const everywhere, then no functions can be
called on the object.  Whence your error.

Again, here you have overlooked the call 'F( C() )'.

Here's a FLAWED analysis, with some facts.  Would appreciate it if you
could return my help in pointing out flaw in your above explanation,
by pointing out flaw(s) in this... :) :)  The facts seem to stand.

The call

   F( C() )

can be resolved as

   F( C().operator A() )         // calls F( A const& )

or

   F( C().operator B() )         // calls F( B const& )

Data point matrix (apparently these are facts):

   case   const on op A    const on op B    result
     0      no                no              Ambigious, F(A) or F(B)
     1      no               YES              Ambigious, F(A) or F(B)
     2     YES                no              F(B)
     3     YES               YES              Ambigious, F(A) or F(B)

The most interesting case seems to be case 1.  Here, from naïve point
of view, the class derivation relationship wants F(B), most specific,
but the extra const "conversion" wants F(A), fewest "conversions".
However, let's first consider cases 0 and 3, where op A() and op B()
compete directly, on equal footing, so to speak.

Hm, standard, what do you say?

OK, §13.3.3.2/2 is the top-level of how to rank implicit conversion
sequences, paraphrased:

    Best:    Standard conversion sequence
    So-so:   User defined conversion sequence
    Worst:   Ellipsis conversion sequence (a "..." formal argument)

What we have is two user defined conversion sequences to be compared
(from C to A or B result), so that top level ranking doesn't come into
play directly, but it can affect rankings for sub-sequences.  For a
user-defined conversion sequence is defined by §13.3.3.1.2 to consist
of a standard conversion sequence followed by a (single) user defined
conversion followed by a standard conversion sequence.  Sort of like
pre-conversion adjustment, real conversion, and post fixups.

   Case 0
   Seq    Pre           Real (user)    Post               Calls
    s1    identity       op A()        A -> A const&      F(A const&)
    s2    identity       op B()        B -> A const&      F(A const&)
    s3    identity       op B()        B -> B const&      F(B const&)

s2 is ranked lower than s3 because the post-conversion of s3 (recall,
the post conversion is a standard conversion sequence, not involving
any calls) is more specific, at least according to my reading of
§13.3.3.2/4.  So that leaves s1 and s3 to choose from.

But s1 and s3 involve the same kinds of conversions.

We do not even need to know the exact rules.  The specificity of the
result of s3 does not enter into the ranking because that specificity
is not of a standard conversion sequence: specificity of class affects
only ranking of standard conversion (like, you have B, then a B
result, doing nothing, is better than direct "upcast" to A, at least
according to my reading of §13.3.3.2/4).  So in this case s1 and s3,
as I understand it, ends up with same rank, and the call is ambigious.

*A key notion*: that "closeness" in inheritance only affects ranking
of standard conversions, not ranking of a user-defined conversion.

Analysis for case 3 would be the same, so, case 1 then:

   Case 1
   Seq    Pre           Real (user)    Post               Calls
    s1    identity       op A()        A -> A const&      F(A const&)
    s2    C -> C const   op B()        B -> A const&      F(A const&)
    s3    C -> C const   op B()        B -> B const&      F(B const&)

Here again s2 is ranked lower than s3.  But it would seem that s1
should prevail, should be ranked higher than s3, because of the const
conversion in the pre-conversion.  However, §13.3.3.2/3 tells us that

   "User-defined conversion sequence U1 is a better conversion sequence
   than another user-defined conversion sequence U2 if they contain the
   same user-defined conversion function or constructor and if the
   second standard conversion sequence [the "Post" above] of U1 is
   better than the second standard conversion sequence of U2."

And it seems there is no other criterion for top-level ranking of user
defined conversion sequences.

Hence, in order to for s1 to be better than s3 it would have to
involve the same conversion operator, and it would have to be better
in the post-conversion.  The pre-conversion is simply ignored, wrt.
ranking.  The result is that s1 and s3 are ranked equally, ambigious.

Finally, analysis for case 2:

   Case 2
   Seq    Pre           Real (user)    Post               Calls
    s1    C -> C const   op A()        A -> A const&      F(A const&)
    s2    identity       op B()        B -> A const&      F(A const&)
    s3    identity       op B()        B -> B const&      F(B const&)

But this analysis fails, in the sense that it tells us that since s1
and s3 involve different user defined conversion functions, neither
one is ranked better than the other.  So according to this analysis
they "should" be equally ranked, ambigious.  But they're not.

Cheers,

- Alf (perplexed, perhaps needs to read Andrei's article again!)

--
A: Because it messes up the order in which people normally read text.
Q: Why is it such a bad thing?
A: Top-posting.
Q: What is the most annoying thing on usenet and in e-mail?- Nascondi testo tra virgolette -

- Mostra testo tra virgolette -

Thanks for the detailed analysis Mr. Steinbach.
I tried to view this issue from a different perspective and I wrote
the same code using converting constructors instead of conversion ops.
I feel that what I wrote "means" the same exact thing... but it does
not compile....

/*
// original with conversion ops
struct B {};

struct C : B {};

struct A
{
operator B() const { return B(); }
operator C() { return C(); }
};
*/

// different code with conversion constructors
struct A {};

struct B
{
B() {}
B( A const & ) {}
};

struct C : B
{
C() {}
C( A & ) {}
};
//

void F( B const & ) {}
void F( C const & ) {}

int main()
{
A obj( ( A() ) );
F( obj );
}


So I'm left with some doubts... Anyway here's the part of
Mr.Alexandrescu's article that still leaves me puzzled:

"Why the equal motivation? This is because the non-const to const
conversion is "frictionless" as far as selecting member functions is
concerned.

The need, therefore, is to give the compiler more "motivation" to
choose the first route than the second. That's where the inheritance
kicks in. Now the compiler says: "Ok, I guess I could go through
ConstantString or TemporaryString... but wait, the derived class
TemporaryString is a better match!"

The rule in action here is that matching a derived class is considered
better than matching a base class when selecting a function from an
overloaded set."

from
http://www.ddj.com/cpp/184403855

Thanks again,
Francesco

P.S.
Would you feel that posting also to clc++mod would get some other
interesting insights?
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,042
Latest member
icassiem

Latest Threads

Top