[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.c++

Default operators.

jason.cipriani@gmail.com

12/9/2008 5:23:00 AM

I've come across a few things that were new to me recently, that I'm
confused about. All are related to various default operators.

Consider:

class BoolTest {
public:
operator bool () const;
bool operator ! () const;
bool operator == (bool) const;
};

Now, in the following code, the expected operators are called:

BoolTest b;
if (b) ; // operator bool
if (!b) ; // operator !
if (b == true) ; // operator ==

But now; if I remove operator bool from the class:

BoolTest b;
if (b) ; // illegal
if (!b) ; // operator !
if (b == true) ; // operator ==

There I would have expected "if (b)" to default to either "!
operator !" or just to use operator ==. It doesn't, but the reason I
would have expected "! operator !" is the opposite seems to be true,
"operator !" defaults to "! operator bool". The reason I would have
expected "operator ==" is because I always thought "if (b)" was
shorthand for "if (b == true)", but it looks like that's not correct?
They clearly have different semantics. If I remove operator ! from the
class (leaving bool and ==):

BoolTest b;
if (b) ; // operator bool
if (!b) ; // operator bool
if (b == true) ; // operator ==

Now, making this more confusing, if I remove operator == from the
class (leaving bool and !):

BoolTest b;
if (b) ; // operator bool
if (!b) ; // operator !
if (b == true) ; // operator bool

And, of course, if I *only* provide operator bool, the default ! and
== do the sensible thing:

BoolTest b;
if (b) ; // operator bool
if (!b) ; // operator bool
if (b == true) ; // operator bool


So, what I've learned here is:

* Default operator ! is "! operator bool"
* Default operator bool is *not* "! operator !"
* Compiler prefers a matching operator == over a matching conversion
operator.

My question here is: Where in the standard does it define all this
stuff? The rules of what operators are used by default seem kind of
complex. The reason for wanting to know is more than just a thought
exercise; it's useful, for example, to know that I can confidently
implement only operator bool, and save myself the trouble of
implementing other operators that are defined to have sensible default
behavior. It makes things like explicitly defining an "operator !"
that returns the opposite of "operator bool" silly, even though it's
frequently done for some reason.


The other thing that I've confused myself with is default conversion
constructors, assignment operators, and conversion operators. There's
some really weird stuff here that I wouldn't mind having a few solid
rules to explain:

class Y {
public:
Y ();
Y (const X &);
Y & operator = (const X &);
};

class X {
public:
operator Y () const;
};

Now consider this code:

X x;
Y a(x); // Y(X&)
Y b = x; // compiler rejects!
Y c;
c = x; // operator =
c = (Y)x; // Y(X&)

I was surprised to see the compiler reject "Y b = x". Comeau had a
clear error message that stated the problem was that there were two
possible conversions "Y(X&)" and "x::operator Y". Yet, this error
message seemed contradictory. "c = (Y)x" was accepted, even though I
would have expected the same error there (in that case Y(X) and
x::operator Y did not have the same priority, Y(X) was preferred).
Furthermore, I always assumed the copy assignment "Y b = x" would do
the exact same thing as the copy constructor, yet the compiler did not
complain about multiple matches for "Y a(x)" nor did it prefer Y(X)
for "Y b = x". Under the assumption that convert+construct and copy
assign are the same (and both do call the conversion constructor, copy
assign does not use assignment operator), then those two observations
seem to contradict eachother.

I know that "Y a(x)" *can* use "X::operator Y" (it does below), but
for some reason that does not cause an ambiguity with "Y a(x)" whereas
it does cause an ambiguity with "Y b = x". What's the deal with that?
Why can the compiler decide for "Y a(x)" while not being able to make
up it's mind about "Y b = x"?

Another unexpected result is when I remove operator = (leaving the
rest of the original operators):

X x;
Y a(x); // Y(X&)
Y b = x; // compiler still rejects.
Y c;
c = x; // compiler rejects!
c = (Y)x; // Y(X&)

I was surprised to see the compiler reject the "c = x". I expected
assignment to fall back on "X::operator Y" and do the implicit
conversion, i.e. "c = (Y)x", but it did not attempt that.

Now, the weirdest one, that seems to destroy any remaining bits of
hope that I had for understanding this is, if I use all the original
operators, but remove the "Y::Y(const X&)" constructor (note
X::operator Y() still defined):

X x;
Y a(x); // X::operator Y
Y b = x; // X::operator Y
Y c;
c = x; // operator =
c = (Y)x; // X::operator Y

The compiler accepts *everything*, and the defaults are all
consistent:

The things I learned here are:

* When performing assignments: "Y::Y(const X&)" is not preferred
over "X::operator Y() const", nor vice versa. They have equal
priority.
* When performing conversions, on the other hand: "Y::Y(const X&)"
is preferred over "X::operator Y() const". The Y() constructor will be
used, not X's conversion.
* When performing conversion during construction , "Y::Y(const X&)"
is also preferred over "X::operator Y() const".
* "Y = X" apparently does not attempt an implicit conversion even if
a conversion "X::operator Y" is defined. It will fail if "Y::operator =
(X)" is not defined.
* And... well I'm trying to add rules to this list as I observe them
but I'm only succeeding in confusing the crap out of myself. I can't
continue this list.


Is there an actual set of simple rules somewhere that can clearly
define all the behavior I'm observing? (In particular, the well-
defined "Y a(x)" vs. ambiguous "Y b = x", given that "Y a(x)" *can*
fall back on X::operator Y, is one of the more confusing
observations). This is also useful for more than just a thought
experiment. I'm trying to figure out what the minimal set of operators
I need to implement to have a "well-behaved" class is -- the goal is
to not have to maintain code that is identical to the default behavior
anyways.


Thanks, and sorry if the examples got a little hectic -- I really
started to lose track of what was going on around the time I commented
out Y::Y(const X&).

Jason
4 Answers

Kai-Uwe Bux

12/9/2008 8:19:00 AM

0

jason.cipriani@gmail.com wrote:

> I've come across a few things that were new to me recently, that I'm
> confused about. All are related to various default operators.
>
> Consider:
>
> class BoolTest {
> public:
> operator bool () const;
> bool operator ! () const;
> bool operator == (bool) const;
> };
>
> Now, in the following code, the expected operators are called:
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator !
> if (b == true) ; // operator ==
>
> But now; if I remove operator bool from the class:
>
> BoolTest b;
> if (b) ; // illegal
> if (!b) ; // operator !
> if (b == true) ; // operator ==
>
> There I would have expected "if (b)" to default to either "!
> operator !" or just to use operator ==. It doesn't, but the reason I
> would have expected "! operator !" is the opposite seems to be true,
> "operator !" defaults to "! operator bool". The reason I would have
> expected "operator ==" is because I always thought "if (b)" was
> shorthand for "if (b == true)", but it looks like that's not correct?

Yup, that's not correct.

> They clearly have different semantics. If I remove operator ! from the
> class (leaving bool and ==):
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator bool
> if (b == true) ; // operator ==
>
> Now, making this more confusing, if I remove operator == from the
> class (leaving bool and !):
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator !
> if (b == true) ; // operator bool

You might want to try

if ( true == b ) ;

:-)

> And, of course, if I *only* provide operator bool, the default ! and
> == do the sensible thing:
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator bool
> if (b == true) ; // operator bool
>
>
> So, what I've learned here is:
>
> * Default operator ! is "! operator bool"
> * Default operator bool is *not* "! operator !"
> * Compiler prefers a matching operator == over a matching conversion
> operator.

You learned partially wrong. There is no "default operator !". What happens
is a user-defined conversion to bool. The compiler prefers a match without
the need for a conversion to a match that involves a conversion. (If there
are several conversion sequences available, things get complicated.)


> My question here is: Where in the standard does it define all this
> stuff?

Chapter 13 on overloading; in particular 13.5 deals with overloading
operators.


> The rules of what operators are used by default seem kind of
> complex.

They are less complex if you phrase them in terms of conversions (which you
should). They are still numerous, though.


> The reason for wanting to know is more than just a thought
> exercise; it's useful, for example, to know that I can confidently
> implement only operator bool, and save myself the trouble of
> implementing other operators that are defined to have sensible default
> behavior. It makes things like explicitly defining an "operator !"
> that returns the opposite of "operator bool" silly, even though it's
> frequently done for some reason.

It's done when operator bool is not defined, which can be a good thing to
avoid unwanted implicit conversions. E.g., you will find operator void*
so that you can do

if (x) ;

but you cannot do

x + true;

However, if subsequent conversions to bool are blocked, you also need
operator!.

(There are even more complicated hacks to get if(x) without unwanted
conversions.)


[snip remainder]

Maybe, I get to the rest some other time.



Best

Kai-Uwe Bux

Andrey Tarasevich

12/9/2008 9:39:00 PM

0

jason.cipriani@gmail.com wrote:
>
> class BoolTest {
> public:
> operator bool () const;
> bool operator ! () const;
> bool operator == (bool) const;
> };
>
> Now, in the following code, the expected operators are called:
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator !
> if (b == true) ; // operator ==
>
> But now; if I remove operator bool from the class:
>
> BoolTest b;
> if (b) ; // illegal
> if (!b) ; // operator !
> if (b == true) ; // operator ==
>
> There I would have expected "if (b)" to default to either "!
> operator !" or just to use operator ==. It doesn't, but the reason I
> would have expected "! operator !" is the opposite seems to be true,
> "operator !" defaults to "! operator bool".
>
> They clearly have different semantics. If I remove operator ! from the
> class (leaving bool and ==):
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator bool
> if (b == true) ; // operator ==
>

Well, that's how the language is defined. 'if' statement implicitly
converts the condition-expression to type 'bool'. The same is true for
operator '!' - its operand is implicitly converted to type 'bool'.

Built-in types are converted to 'bool' by using standard boolean
conversions. As for user-defined types, they should either have an
operator that converts them to 'bool' directly or some other conversion
operator to some other type that can in turn be then converted to 'bool'
using standard boolean conversion (types like 'int', 'void*' etc.)

In case of '!' operator, if you overload it, your overloaded version
takes precedence. That's it.

> The reason I would have
> expected "operator ==" is because I always thought "if (b)" was
> shorthand for "if (b == true)", but it looks like that's not correct?

No. In accordance with what I said above, 'if (b)' can be best described
as a shorthand for 'if ((bool) b)' (where '(bool)' stands for an
implicit conversion performed by the compiler). And if 'b' happens to
have a built-in type subject to standard boolean conversion, it really
is equivalent to 'if (b != 0)', not to 'if (b == true)', as you
incorrectly state. But, once again this is only applicable to built-in
types. Don't expect the overloaded '!=' operator to be called for your
user-defined type.

> Now, making this more confusing, if I remove operator == from the
> class (leaving bool and !):
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator !
> if (b == true) ; // operator bool
>
> And, of course, if I *only* provide operator bool, the default ! and
> == do the sensible thing:
>
> BoolTest b;
> if (b) ; // operator bool
> if (!b) ; // operator bool
> if (b == true) ; // operator bool
>
>
> So, what I've learned here is:
>
> * Default operator ! is "! operator bool"

Your use of the term "default" is a bit confusing. Apparently you are
talking about the _built-in_ operator '!'.

So, yes, the built-in '!' converts its argument to type 'bool', i.e.
'!b' basically means '!(bool) b' (where, once again, '(bool)' stands for
an implicit conversion performed by the compiler).

And it is not really correct to describe the standard boolean conversion
as an 'operator bool'. As I said above, even for a user-defined type '!'
might actually mean '!(bool)(operator void*)', depending on the type.

> * Default operator bool is *not* "! operator !"

No, of course. Conversion to 'bool' is either a standard boolean
conversion (e.g. '!b' means 'b != 0'), user-defined conversion to
'bool', if available (e.g. '!b' means '!b.operator bool()'), otherwise a
combination of a user-defined conversion and a standard boolean
conversion (e.g. '!b' means 'b.operator void*() != 0').

> * Compiler prefers a matching operator == over a matching conversion
> operator.

Er... Not exactly. All this is governed by overload resolution rules,
where the general principle is that if the compiler can call several
different operators, the one that requires less (or no) conversions
wins. What you had in your case, was _not_ the competition between the
user-defined '==' and user defined 'operator bool', as you seem to
assume. In reality, what you had in your case was the competition
between the user-defined '==' and the built-in '=='. I.e. in response to

b == true

the compiler can either do

// user-defined ==
b.operator ==(true)

or

// built-in ==
(bool) b == true
// which means in your case
b.operator bool() == true

The user-defined version of '==' can be called immediately, while the
built-in '==' requires 'b' be implicitly converted to 'bool' first. The
need for this implicit conversion is what makes the built-in '==' to
lose the overload resolution.

> My question here is: Where in the standard does it define all this
> stuff?

6.4 Selection statements
5.3 Unary operators
4.12 Boolean conversions

and, finally, the big one

13 Overloading

especially

13.3 Overload resolution

> The rules of what operators are used by default seem kind of
> complex.

Not really. You seem to have overcomplicated them yourself, by trying to
base it on the number of your own rather complex (and incorrect)
assumptions.

> The other thing that I've confused myself with is default conversion
> constructors, assignment operators, and conversion operators. There's
> some really weird stuff here that I wouldn't mind having a few solid
> rules to explain:
>
> class Y {
> public:
> Y ();
> Y (const X &);
> Y & operator = (const X &);
> };
>
> class X {
> public:
> operator Y () const;
> };
>
> Now consider this code:
>
> X x;
> Y a(x); // Y(X&)
> Y b = x; // compiler rejects!
> Y c;
> c = x; // operator =
> c = (Y)x; // Y(X&)
>
> I was surprised to see the compiler reject "Y b = x". Comeau had a
> clear error message that stated the problem was that there were two
> possible conversions "Y(X&)" and "x::operator Y".

OK.

> Yet, this error
> message seemed contradictory. "c = (Y)x" was accepted, even though I
> would have expected the same error there (in that case Y(X) and
> x::operator Y did not have the same priority, Y(X) was preferred).

This is not the same thing. In the first case you were trying to perform
a copy-initialization, which by definition calls the copy-constructor
'Y::Y(const Y&)'. This constructor requires an argument of type 'Y',
while you are supplying one of type 'X'. The compiler tries to perform
an _implicit_ conversion from 'Y' to 'X'. This conversion happens to be
ambiguous: there are two equally good ways to perform it. Hence the error.

In the second case you are requesting an _explicit_ conversion by using
a cast operator '(Y)'. This operator is immediately resolved to
'operator Y' (when one is available, as is in your case). Therefore, no
error.

Add an explicit cast to the first case

Y b = (Y) x;

and the error will go away there as well.

> Furthermore, I always assumed the copy assignment "Y b = x" would do
> the exact same thing as the copy constructor,

Firstly, this is not an assignment. It is an initialization. This is so
called copy-initialization, as I said above.

Secondly, it does invoke the copy constructor (again as I described
above). It is not clear to me what is not "the same thing" here.

> yet the compiler did not
> complain about multiple matches for "Y a(x)"

This is direct-initialization. It is resolved by using overload
resolution on the target class's constructors. In this case the overload
resolution has the choice of calling 'Y::Y(const X&)', which can be done
right away, without any additional implicit conversions. This detail (no
conversion) makes it the best candidate to call.

> nor did it prefer Y(X)
> for "Y b = x".

It has no reason to "prefer" it. In this case the compiler has two
_equally_ _good_ conversion sequences.

> Under the assumption that convert+construct and copy
> assign are the same (and both do call the conversion constructor, copy
> assign does not use assignment operator), then those two observations
> seem to contradict eachother.

Well, this observation doesn't make much sense terminologically. Again,
what you refer to as "copy assign" is really copy-initialization, which
means 'convert + copy construct'. What you refer to as
"convert+construct" must be direct-initialization, which means just
'construct' (converting the arguments, if necessary).

> I know that "Y a(x)" *can* use "X::operator Y" (it does below),

OK, let's take a look below...

> but
> for some reason that does not cause an ambiguity with "Y a(x)" whereas
> it does cause an ambiguity with "Y b = x". What's the deal with that?
> Why can the compiler decide for "Y a(x)" while not being able to make
> up it's mind about "Y b = x"?

This is the difference between copy-initialization and
direct-initialization. In general case (and in your case) these two
kinds of initialization work differently. See 8.5 Initializers.

> Another unexpected result is when I remove operator = (leaving the
> rest of the original operators):
>
> X x;
> Y a(x); // Y(X&)
> Y b = x; // compiler still rejects.
> Y c;
> c = x; // compiler rejects!
> c = (Y)x; // Y(X&)
>
> I was surprised to see the compiler reject the "c = x". I expected
> assignment to fall back on "X::operator Y" and do the implicit
> conversion, i.e. "c = (Y)x", but it did not attempt that.

Why should it? What you have now with the assignment is _exactly_ the
same as with the copy-construction 'Y b = x'. Both fail for exactly the
same reasons. You ask the compiler to perform an _implicit_ conversion,
but it happens to be ambiguous.

Once you add the _explicit_ '(Y)', it solves the problem just like
before, by resolving the ambiguity. YOu are essentially telling the
compiler: "of these two conversion methods I want this one", and the
ambiguity is gone.

> Now, the weirdest one, that seems to destroy any remaining bits of
> hope that I had for understanding this is, if I use all the original
> operators, but remove the "Y::Y(const X&)" constructor (note
> X::operator Y() still defined):
>
> X x;
> Y a(x); // X::operator Y
> Y b = x; // X::operator Y
> Y c;
> c = x; // operator =
> c = (Y)x; // X::operator Y
>
> The compiler accepts *everything*, and the defaults are all
> consistent:

Well, duh! You removed one of the two conflicting, equally good
conversion sequences and, as one's expect, the ambiguity disappeared.
Now the compiler has only one possible conversion sequence. No ambiguity
- no problems.

> The things I learned here are:
>
> * When performing assignments: "Y::Y(const X&)" is not preferred
> over "X::operator Y() const", nor vice versa. They have equal
> priority.

Yes. It equally applies to both assignment and copy-construction.

> * When performing conversions, on the other hand: "Y::Y(const X&)"
> is preferred over "X::operator Y() const". The Y() constructor will be
> used, not X's conversion.

Er... It should probably be "When performing direct-initialization..."
and it is true because direct-initialization, by definition, uses all
available constructors of the target class.

> * When performing conversion during construction , "Y::Y(const X&)"
> is also preferred over "X::operator Y() const".

This says the same thing as the previous one, the way I understood it.

> * "Y = X" apparently does not attempt an implicit conversion even if
> a conversion "X::operator Y" is defined. It will fail if "Y::operator =
> (X)" is not defined.

Incorrect. It does attempt to perform the conversion. In your case it
failed because the conversion was ambiguous. In your last experiment,
instead of removing 'Y::Y(const X&)' you could've removed 'Y::operator
X()' and the code would also compile. You see - 'Y::operator X()' is not
defined, but 'Y = X' compiles.

> Is there an actual set of simple rules somewhere that can clearly
> define all the behavior I'm observing? (In particular, the well-
> defined "Y a(x)" vs. ambiguous "Y b = x", given that "Y a(x)" *can*
> fall back on X::operator Y, is one of the more confusing
> observations). This is also useful for more than just a thought
> experiment. I'm trying to figure out what the minimal set of operators
> I need to implement to have a "well-behaved" class is -- the goal is
> to not have to maintain code that is identical to the default behavior
> anyways.

The basic rule is: if you have an user-defined function that matches
what you are trying to call _exactly_, this user-defined function is
called, "outcompeting" all other variants during overload resolution.
But if you have no exact match, things get more tricky: you need to
collect all possible variants and see if there's one that "looks better"
than others. What "looks better" mean is defined in the standard and
involves analyzing the explicit conversion sequences required for the
call. It might get tricky at times.

Also, take a look at 8.5 to get the idea of copy-initialization and
direct-initialization and differences between them.

--
Best regards,
Andrey Tarasevich

jason.cipriani@gmail.com

12/11/2008 7:23:00 AM

0

On Dec 9, 3:18 am, Kai-Uwe Bux <jkherci...@gmx.net> wrote:
> jason.cipri...@gmail.com wrote:
> > I've come across a few things that were new to me recently, that I'm
> > confused about. All are related to various default operators.
>
> > Consider:
>
> >  class BoolTest {
> >  public:
> > operator bool () const;
> > bool operator ! () const;
> > bool operator == (bool) const;
> >  };
>
> > Now, in the following code, the expected operators are called:
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator ==
>
> > But now; if I remove operator bool from the class:
>
> >  BoolTest b;
> >  if (b) ;  // illegal
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator ==
>
> > There I would have expected "if (b)" to default to either "!
> > operator !" or just to use operator ==. It doesn't, but the reason I
> > would have expected "! operator !" is the opposite seems to be true,
> > "operator !" defaults to "! operator bool".  The reason I would have
> > expected "operator ==" is because I always thought "if (b)" was
> > shorthand for "if (b == true)", but it looks like that's not correct?
>
> Yup, that's not correct.
>
> > They clearly have different semantics. If I remove operator ! from the
> > class (leaving bool and ==):
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator bool
> >  if (b == true) ; // operator ==
>
> > Now, making this more confusing, if I remove operator == from the
> > class (leaving bool and !):
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator bool
>
> You might want to try
>
>    if ( true == b ) ;
>
> :-)
>
> > And, of course, if I *only* provide operator bool, the default ! and
> > == do the sensible thing:
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator bool
> >  if (b == true) ; // operator bool
>
> > So, what I've learned here is:
>
> >   * Default operator ! is "! operator bool"
> >   * Default operator bool is *not* "! operator !"
> >   * Compiler prefers a matching operator == over a matching conversion
> > operator.
>
> You learned partially wrong. There is no "default operator !". What happens
> is a user-defined conversion to bool. The compiler prefers a match without
> the need for a conversion to a match that involves a conversion. (If there
> are several conversion sequences available, things get complicated.)
>
> > My question here is: Where in the standard does it define all this
> > stuff?
>
> Chapter 13 on overloading; in particular 13.5 deals with overloading
> operators.
>
> > The rules of what operators are used by default seem kind of
> > complex.
>
> They are less complex if you phrase them in terms of conversions (which you
> should). They are still numerous, though.
>
> > The reason for wanting to know is more than just a thought
> > exercise; it's useful, for example, to know that I can confidently
> > implement only operator bool, and save myself the trouble of
> > implementing other operators that are defined to have sensible default
> > behavior. It makes things like explicitly defining an "operator !"
> > that returns the opposite of "operator bool" silly, even though it's
> > frequently done for some reason.
>
> It's done when operator bool is not defined, which can be a good thing to
> avoid unwanted implicit conversions. E.g., you will find operator void*
> so that you can do
>
>   if (x) ;
>
> but you cannot do
>
>   x + true;
>
> However, if subsequent conversions to bool are blocked, you also need
> operator!.
>
> (There are even more complicated hacks to get if(x) without unwanted
> conversions.)
>
> [snip remainder]
>
> Maybe, I get to the rest some other time.


Thanks a lot for all the information; I've been reading through the
standard a little and everything seems to make a lot more sense now.

Jason




> Best
>
> Kai-Uwe Bux

jason.cipriani@gmail.com

12/11/2008 7:25:00 AM

0

On Dec 9, 4:39 pm, Andrey Tarasevich <andreytarasev...@hotmail.com>
wrote:
> jason.cipri...@gmail.com wrote:
>
> >  class BoolTest {
> >  public:
> >    operator bool () const;
> >    bool operator ! () const;
> >    bool operator == (bool) const;
> >  };
>
> > Now, in the following code, the expected operators are called:
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator ==
>
> > But now; if I remove operator bool from the class:
>
> >  BoolTest b;
> >  if (b) ;  // illegal
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator ==
>
> > There I would have expected "if (b)" to default to either "!
> > operator !" or just to use operator ==. It doesn't, but the reason I
> > would have expected "! operator !" is the opposite seems to be true,
> > "operator !" defaults to "! operator bool".  
>
>  >
>  > They clearly have different semantics. If I remove operator ! from the
>  > class (leaving bool and ==):
>  >
>  >  BoolTest b;
>  >  if (b) ;  // operator bool
>  >  if (!b) ;  // operator bool
>  >  if (b == true) ; // operator ==
>  >
>
> Well, that's how the language is defined. 'if' statement implicitly
> converts the condition-expression to type 'bool'. The same is true for
> operator '!' - its operand is implicitly converted to type 'bool'.
>
> Built-in types are converted to 'bool' by using standard boolean
> conversions. As for user-defined types, they should either have an
> operator that converts them to 'bool' directly or some other conversion
> operator to some other type that can in turn be then converted to 'bool'
> using standard boolean conversion (types like 'int', 'void*' etc.)
>
> In case of '!' operator, if you overload it, your overloaded version
> takes precedence. That's it.
>
> > The reason I would have
> > expected "operator ==" is because I always thought "if (b)" was
> > shorthand for "if (b == true)", but it looks like that's not correct?
>
> No. In accordance with what I said above, 'if (b)' can be best described
> as a shorthand for 'if ((bool) b)' (where '(bool)' stands for an
> implicit conversion performed by the compiler). And if 'b' happens to
> have a built-in type subject to standard boolean conversion, it really
> is equivalent to 'if (b != 0)', not to 'if (b == true)', as you
> incorrectly state. But, once again this is only applicable to built-in
> types. Don't expect the overloaded '!=' operator to be called for your
> user-defined type.
>
> > Now, making this more confusing, if I remove operator == from the
> > class (leaving bool and !):
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator !
> >  if (b == true) ; // operator bool
>
> > And, of course, if I *only* provide operator bool, the default ! and
> > == do the sensible thing:
>
> >  BoolTest b;
> >  if (b) ;  // operator bool
> >  if (!b) ;  // operator bool
> >  if (b == true) ; // operator bool
>
> > So, what I've learned here is:
>
> >   * Default operator ! is "! operator bool"
>
> Your use of the term "default" is a bit confusing. Apparently you are
> talking about the _built-in_ operator '!'.
>
> So, yes, the built-in '!' converts its argument to type 'bool', i.e.
> '!b' basically means '!(bool) b' (where, once again, '(bool)' stands for
> an implicit conversion performed by the compiler).
>
> And it is not really correct to describe the standard boolean conversion
> as an 'operator bool'. As I said above, even for a user-defined type '!'
> might actually mean '!(bool)(operator void*)', depending on the type.
>
> >   * Default operator bool is *not* "! operator !"
>
> No, of course. Conversion to 'bool' is either a standard boolean
> conversion (e.g. '!b' means 'b != 0'), user-defined conversion to
> 'bool', if available (e.g. '!b' means '!b.operator bool()'), otherwise a
> combination of a user-defined conversion and a standard boolean
> conversion (e.g. '!b' means 'b.operator void*() != 0').
>
> >   * Compiler prefers a matching operator == over a matching conversion
> > operator.
>
> Er... Not exactly. All this is governed by overload resolution rules,
> where the general principle is that if the compiler can call several
> different operators, the one that requires less (or no) conversions
> wins. What you had in your case, was _not_ the competition between the
> user-defined '==' and user defined 'operator bool', as you seem to
> assume. In reality, what you had in your case was the competition
> between the user-defined '==' and the built-in '=='. I.e. in response to
>
>    b == true
>
> the compiler can either do
>
>    // user-defined ==
>    b.operator ==(true)
>
> or
>
>    // built-in ==
>    (bool) b == true
>    // which means in your case
>    b.operator bool() == true
>
> The user-defined version of '==' can be called immediately, while the
> built-in '==' requires 'b' be implicitly converted to 'bool' first. The
> need for this implicit conversion is what makes the built-in '==' to
> lose the overload resolution.
>
> > My question here is: Where in the standard does it define all this
> > stuff?
>
>    6.4 Selection statements
>    5.3 Unary operators
>    4.12 Boolean conversions
>
> and, finally, the big one
>
>    13 Overloading
>
> especially
>
>    13.3 Overload resolution
>
> > The rules of what operators are used by default seem kind of
> > complex.
>
> Not really. You seem to have overcomplicated them yourself, by trying to
> base it on the number of your own rather complex (and incorrect)
> assumptions.
>
> > The other thing that I've confused myself with is default conversion
> > constructors, assignment operators, and conversion operators. There's
> > some really weird stuff here that I wouldn't mind having a few solid
> > rules to explain:
>
> >  class Y {
> >  public:
> >    Y ();
> >    Y (const X &);
> >    Y & operator = (const X &);
> >  };
>
> >  class X {
> >  public:
> >    operator Y () const;
> >  };
>
> > Now consider this code:
>
> >    X x;
> >    Y a(x); // Y(X&)
> >    Y b = x; // compiler rejects!
> >    Y c;
> >    c = x; // operator =
> >    c = (Y)x; // Y(X&)
>
> > I was surprised to see the compiler reject "Y b = x". Comeau had a
> > clear error message that stated the problem was that there were two
> > possible conversions "Y(X&)" and "x::operator Y".
>
> OK.
>
> > Yet, this error
> > message seemed contradictory. "c = (Y)x" was accepted, even though I
> > would have expected the same error there (in that case Y(X) and
> > x::operator Y did not have the same priority, Y(X) was preferred).
>
> This is not the same thing. In the first case you were trying to perform
> a copy-initialization, which by definition calls the copy-constructor
> 'Y::Y(const Y&)'. This constructor requires an argument of type 'Y',
> while you are supplying one of type 'X'. The compiler tries to perform
> an _implicit_ conversion from 'Y' to 'X'. This conversion happens to be
> ambiguous: there are two equally good ways to perform it. Hence the error.
>
> In the second case you are requesting an _explicit_ conversion by using
> a cast operator '(Y)'. This operator is immediately resolved to
> 'operator Y' (when one is available, as is in your case). Therefore, no
> error.
>
> Add an explicit cast to the first case
>
>    Y b = (Y) x;
>
> and the error will go away there as well.
>
> > Furthermore, I always assumed the copy assignment "Y b = x" would do
> > the exact same thing as the copy constructor,
>
> Firstly, this is not an assignment. It is an initialization. This is so
> called copy-initialization, as I said above.
>
> Secondly, it does invoke the copy constructor (again as I described
> above). It is not clear to me what is not "the same thing" here.
>
> > yet the compiler did not
> > complain about multiple matches for "Y a(x)"
>
> This is direct-initialization. It is resolved by using overload
> resolution on the target class's constructors. In this case the overload
> resolution has the choice of calling 'Y::Y(const X&)', which can be done
> right away, without any additional implicit conversions. This detail (no
> conversion) makes it the best candidate to call.
>
> > nor did it prefer Y(X)
> > for "Y b = x".
>
> It has no reason to "prefer" it. In this case the compiler has two
> _equally_ _good_ conversion sequences.
>
> > Under the assumption that convert+construct and copy
> > assign are the same (and both do call the conversion constructor, copy
> > assign does not use assignment operator), then those two observations
> > seem to contradict eachother.
>
> Well, this observation doesn't make much sense terminologically. Again,
> what you refer to as "copy assign" is really copy-initialization, which
> means 'convert + copy construct'. What you refer to as
> "convert+construct" must be direct-initialization, which means just
> 'construct' (converting the arguments, if necessary).
>
> > I know that "Y a(x)" *can* use "X::operator Y" (it does below),
>
> OK, let's take a look below...
>
> > but
> > for some reason that does not cause an ambiguity with "Y a(x)" whereas
> > it does cause an ambiguity with "Y b = x". What's the deal with that?
> > Why can the compiler decide for "Y a(x)" while not being able to make
> > up it's mind about "Y b = x"?
>
> This is the difference between copy-initialization and
> direct-initialization. In general case (and in your case) these two
> kinds of initialization work differently. See 8.5 Initializers.
>
> > Another unexpected result is when I remove operator = (leaving the
> > rest of the original operators):
>
> >    X x;
> >    Y a(x); // Y(X&)
> >    Y b = x; // compiler still rejects.
> >    Y c;
> >    c = x; //  compiler rejects!
> >    c = (Y)x; // Y(X&)
>
> > I was surprised to see the compiler reject the "c = x". I expected
> > assignment to fall back on "X::operator Y" and do the implicit
> > conversion, i.e. "c = (Y)x", but it did not attempt that.
>
> Why should it? What you have now with the assignment is _exactly_ the
> same as with the copy-construction 'Y b = x'. Both fail for exactly the
> same reasons. You ask the compiler to perform an _implicit_ conversion,
> but it happens to be ambiguous.
>
> Once you add the _explicit_ '(Y)', it solves the problem just like
> before, by resolving the ambiguity. YOu are essentially telling the
> compiler: "of these two conversion methods I want this one", and the
> ambiguity is gone.
>
> > Now, the weirdest one, that seems to destroy any remaining bits of
> > hope that I had for understanding this is, if I use all the original
> > operators, but remove the "Y::Y(const X&)" constructor (note
> > X::operator Y() still defined):
>
> >    X x;
> >    Y a(x); // X::operator Y
> >    Y b = x; // X::operator Y
> >    Y c;
> >    c = x; // operator =
> >    c = (Y)x; // X::operator Y
>
> > The compiler accepts *everything*, and the defaults are all
> > consistent:
>
> Well, duh! You removed one of the two conflicting, equally good
> conversion sequences and, as one's expect, the ambiguity disappeared.
> Now the compiler has only one possible conversion sequence. No ambiguity
> - no problems.
>
> > The things I learned here are:
>
> >   * When performing assignments: "Y::Y(const X&)" is not preferred
> > over "X::operator Y() const", nor vice versa. They have equal
> > priority.
>
> Yes. It equally applies to both assignment and copy-construction.
>
> >   * When performing conversions, on the other hand: "Y::Y(const X&)"
> > is preferred over "X::operator Y() const". The Y() constructor will be
> > used, not X's conversion.
>
> Er... It should probably be "When performing direct-initialization..."
> and it is true because direct-initialization, by definition, uses all
> available constructors of the target class.
>
> >   * When performing conversion during construction , "Y::Y(const X&)"
> > is also preferred over "X::operator Y() const".
>
> This says the same thing as the previous one, the way I understood it.
>
> >   * "Y = X" apparently does not attempt an implicit conversion even if
> > a conversion "X::operator Y" is defined. It will fail if "Y::operator =
> > (X)" is not defined.
>
> Incorrect. It does attempt to perform the conversion. In your case it
> failed because the conversion was ambiguous. In your last experiment,
> instead of removing 'Y::Y(const X&)' you could've removed 'Y::operator
> X()' and the code would also compile. You see - 'Y::operator X()' is not
> defined, but 'Y = X' compiles.
>
> > Is there an actual set of simple rules somewhere that can clearly
> > define all the behavior I'm observing? (In particular, the well-
> > defined "Y a(x)" vs. ambiguous "Y b = x", given that "Y a(x)" *can*
> > fall back on X::operator Y, is one of the more confusing
> > observations). This is also useful for more than just a thought
> > experiment. I'm trying to figure out what the minimal set of operators
> > I need to implement to have a "well-behaved" class is -- the goal is
> > to not have to maintain code that is identical to the default behavior
> > anyways.
>
> The basic rule is: if you have an user-defined function that matches
> what you are trying to call _exactly_, this user-defined function is
> called, "outcompeting" all other variants during overload resolution.
> But if you have no exact match, things get more tricky: you need to
> collect all possible variants and see if there's one that "looks better"
> than others. What "looks better" mean is defined in the standard and
> involves analyzing the explicit conversion sequences required for the
> call. It might get tricky at times.
>
> Also, take a look at 8.5 to get the idea of copy-initialization and
> direct-initialization and differences between them.


Thanks a lot for spending so much time on this reply. It's really got
a lot of great information in it. I've been reading through the
relevant parts of the standard, and playing around a little more, and
it's starting to make a lot more sense. Greatly appreciated.

Thanks again,
Jason