J
jason.cipriani
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:
perator 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:
perator 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:
perator 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:
perator 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:
perator Y() still defined):
X x;
Y a(x); // X:
perator Y
Y b = x; // X:
perator Y
Y c;
c = x; // operator =
c = (Y)x; // X:
perator 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:
perator Y() const", nor vice versa. They have equal
priority.
* When performing conversions, on the other hand: "Y::Y(const X&)"
is preferred over "X:
perator 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:
perator Y() const".
* "Y = X" apparently does not attempt an implicit conversion even if
a conversion "X:
perator Y" is defined. It will fail if "Y:
perator =
(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:
perator 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
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:
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:
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:
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:
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:
X x;
Y a(x); // X:
Y b = x; // X:
Y c;
c = x; // operator =
c = (Y)x; // X:
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:
priority.
* When performing conversions, on the other hand: "Y::Y(const X&)"
is preferred over "X:
used, not X's conversion.
* When performing conversion during construction , "Y::Y(const X&)"
is also preferred over "X:
* "Y = X" apparently does not attempt an implicit conversion even if
a conversion "X:
(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:
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