jason.cipriani@gmail.com
12/11/2008 7:25:00 AM
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