Different cast operator called by different compilers
Consider the following short C++ program:
#include <iostream>
class B {
public:
operator bool() const {
return false;
}
};
class B2 : public B {
public:
operator int() {
return 5;
}
};
int main() {
B2 b;
std::cout << std::boolalpha << (bool)b << std::endl;
}
If I compile it on different compilers, I get various results. With Clang 3.4 and GCC 4.4.7 it prints true
, while Visual Studio 2013 prints false
, which means that they call different cast operators at (bool)b
. Which is the correct behavior according to the standard?
In my understanding operator bool()
needs no conversion, while operator int()
would require an int
to bool
conversion, so the compiler should choose the first one. Does const
do something with that, is const-conversion considered more "expensive" by the compiler?
If I remove the const
, all compilers equally produce false
as output.
On the other hand, if I combine the two classes together (both operators will be in the same class) all three compilers will produce true
output.
Solution 1:
The standard states:
A conversion function in a derived class does not hide a conversion function in a base class unless the two functions convert to the same type.
§12.3 [class.conv]
Which means that operator bool
is not hidden by operator int
.
The standard states:
During overload resolution, the implied object argument is indistinguishable from other arguments.
§13.3.3.1 [over.match.funcs]
The "implied object argument" in this case is b
, which is of type B2 &
. operator bool
requires const B2 &
, so the compiler will have to add const to b
to call operator bool
. This -- all other things being equal -- makes operator int
a better match.
The standard states that a static_cast
(which the C-style cast is performing in this instance) can convert to a type T
(in this case int
) if:
the declaration
T t(e);
is well-formed, for some invented temporary variablet
.§5.2.9 [expr.static.cast]
Therefore the int
may be converted to a bool
, and a bool
may equally be converted to a bool
.
The standard states:
The conversion functions of
S
and its base classes are considered. Those non-explicit conversion functions that are not hidden withinS
and yield typeT
or a type that can be converted to typeT
via a standard conversion sequence are candidate functions.§13.3.1.5 [over.match.conv]
So the overload set consists of operator int
and operator bool
. All other things being equal, operator int
is a better match (since you don't have to add constness). Therefore operator int
should be selected.
Note that (perhaps against intuition) the standard does not consider the return type (i.e. the type to which these operators convert) once they have been added to the overload set (as established above), provided the conversion sequence for the arguments of one of them is superior to the conversion sequence for the arguments of the other (which, due to constness, is the case in this instance).
The standard states:
Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
- for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that,
- the context is an initialization by user-defined conversion and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type.
§13.3.3 [over.match.best]
In this case, there is only one argument (the implicit this
parameter). The conversion sequence for B2 &
=> B2 &
(to call operator int
) is superior to B2 &
=> const B2 &
(to call operator bool
), and therefore operator int
is selected from the overload set without regard to the fact that it actually doesn't convert directly to bool
.
Solution 2:
Short
The conversion function operator int()
is selected by clang over operator bool() const
since b
is not const qualified, whereas the conversion operator for bool is.
The short reasoning is that the candidate functions for overload resolution (with implicit object parameter in place), when converting b
to bool
are
operator bool (B2 const &);
operator int (B2 &);
where the second one is a better match since b
is not const qualified.
If both functions share the same qualification (either both const
or not), operator bool
is selected since it provides direct conversion.
Conversion via cast-notation, analyzed step by step
If we agree that the boolean ostream inserter (std::basic_ostream::operator<<(bool val) as per [ostream.inserters.arithmetic]) is called with the value that results from a conversion of b
to bool
we can dig into that conversion.
1. The cast expression
The cast of b to bool
(bool)b
evaluates to
static_cast<bool>(b)
as per C++11, 5.4/4 [expr.cast] since const_cast
is not applicable (not adding or removing const here).
This static conversion is allowed per C++11, 5.2.9/4 [expr.static.cast], if bool t(b);
for an invented variable t is well formed.
Such statements are called direct-initialization as per C++11, 8.5/15 [dcl.init].
2. Direct initialization bool t(b);
Clause 16 of the least mentioned standard paragraph states (emphasis mine):
The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression.
[...]
[...] if the source type is a (possibly cv-qualified) class type, conversion functions are considered.
The applicable conversion functions are enumerated, and the best one is chosen through overload resolution.
2.1 Which conversion functions are available?
The available conversion functions are operator int ()
and operator bool() const
since as C++11, 12.3/5 [class.conv] tells us:
A conversion function in a derived class does not hide a conversion function in a base class unless the two functions convert to the same type.
While C++11, 13.3.1.5/1 [over.match.conv] states:
The conversion functions of S and its base classes are considered.
where S is the class that will be converted from.
2.2 Which conversion functions are applicable?
C++11, 13.3.1.5/1 [over.match.conv] (emphasis mine):
1 [...] Assuming that “cv1 T” is the type of the object being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows: The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T via a standard conversion sequence are candidate functions.
Therefore operator bool () const
is applicable since it is not hidden within B2
and yields a bool
.
The part with emphasis in the last standard quote is relevant for the conversion using operator int ()
since int
is a type that can be converted to bool via standard conversion sequence.
The conversion from int
to bool
is not even a sequence but a plain direct conversion which is allowed per C++11, 4.12/1 [conv.bool]
A prvalue of arithmetic, unscoped enumeration, pointer, or pointer to member type can be converted to a prvalue of type bool. A zero value, null pointer value, or null member pointer value is converted to false; any other value is converted to true.
This means that operator int ()
is applicable as well.
2.3 Which conversion function is selected?
The selection of the appropriate conversion function is performed via overload resolution (C++11, 13.3.1.5/1 [over.match.conv]):
Overload resolution is used to select the conversion function to be invoked.
There is one special "quirk" when it comes to overload resolution for class member functions: the implicit object parameter".
Per C++11, 13.3.1 [over.match.funcs],
[...]both static and non-static member functions have an implicit object parameter[...]
where the type of this parameter for non-static member functions -according to clause 4- is:
“lvalue reference to cv X” for functions declared without a ref-qualifier or with the & ref-qualifier
“rvalue reference to cv X” for functions declared with the && ref-qualifier
where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration.
This means that (per C++11, 13.3.1.5/2 [over.match.conv]), in an initialization by conversion function,
[t]he argument list has one argument, which is the initializer expression. [ Note: This argument will be compared against the implicit object parameter of the conversion functions. —end note ]
The candidate functions for overload resolution are:
operator bool (B2 const &);
operator int (B2 &);
Obviously, operator int ()
is a better match if a conversion is requested using a non-constant object of type B2
since operator bool ()
required a qualification conversion.
If both conversion functions share the same const qualification, overload resolution of those function won't do the trick anymore. In this case, conversion (sequence) ranking comes into place.
3. Why is operator bool ()
selected when both conversion function share the same const qualification?
The conversion from B2
to bool
is an user-defined conversion sequence (C++11, 13.3.3.1.2/1 [over.ics.user])
A user-defined conversion sequence consists of an initial standard conversion sequence followed by a userdefined conversion followed by a second standard conversion sequence.
[...] If the user-defined conversion is specified by a conversion function, the initial standard conversion sequence converts the source type to the implicit object parameter of the conversion function.
C++11, 13.3.3.2/3 [over.ics.rank]
[...] defines a partial ordering of implicit conversion sequences based on the relationships better conversion sequence and better conversion.
[...] 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 or aggregate initialization and the second standard conversion sequence of U1 is better than the second standard conversion sequence of U2.
The second standard conversion is case of operator bool()
is bool
to bool
(identity conversion) whereas the second standard conversion in case of operator int ()
is int
to bool
which is a boolean conversion.
Therefore, the conversion sequence, using operator bool ()
, is better if both conversion function share the same const qualification.
Solution 3:
The C++ bool type has two values - true and false with corresponding values 1 and 0. The inherent confusion can be avoided if you add a bool operator in B2 class that calls base class(B)'s bool operator explicitly, then the output comes as false. Here's my modified program. Then operator bool means operator bool and not operator int by any means.
#include <iostream>
class B {
public:
operator bool() const {
return false;
}
};
class B2 : public B {
public:
operator int() {
return 5;
}
operator bool() {
return B::operator bool();
}
};
int main() {
B2 b;
std::cout << std::boolalpha << (bool)b << std::endl;
}
In your example, (bool) b was trying to call the bool operator for B2, B2 has inherited bool operator, and int operator, by dominance rule, int operator gets called and the inherited bool operator in B2. However, by explicitly having a bool operator in B2 class itself, the problem gets solved.