80

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.

abyss.7
  • 13,882
  • 11
  • 56
  • 100
buc
  • 6,268
  • 1
  • 34
  • 51
  • 3
    As I understand, B2 have 2 operators: B::operator bool() and B2::operator int(). Both operators are not const operators and there is a difference between const and non-const versions. You can add const version of the bool into B and const version of the int to B2 and see the changes. – Tanuki Aug 11 '14 at 10:56
  • 1
    `const` is the key here. Both operators being equally const or not, they behave as expected, the `bool` version "wins". With different constness, always the non-const version "wins" on every platform. With derived classes and different constness of the operators, VS seems to have a bug as it behaves differently than the other two compilers. – buc Aug 11 '14 at 11:05
  • could different default constructors be responsible for the differing behaviour? – ldgorman Aug 11 '14 at 11:15
  • 16
    EDG also prints `true`. If GCC, Clang and EDG all agree and MSVC disagrees that usually means MSVC is wrong. – Jonathan Wakely Aug 11 '14 at 11:29
  • 1
    Note: in general in C++, the return type does *not* participate to overload resolution. – Matthieu M. Aug 11 '14 at 13:14
  • Don't use C-style casts, either. What does "cast to bool" mean for anything other than an integer primitive, anyway? –  Aug 12 '14 at 01:16
  • @Snowman `static_cast` has the same effect, though. The cast is there just for demonstration purposes. – buc Aug 12 '14 at 07:55

4 Answers4

51

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 variable t.

§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 within S and yield type T or a type that can be converted to type T 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.

  • **EXCELLENT!!** Btw, where did you find and read these standard documentation? I've always wondered where great people such as you find the proper part of standard doc for each questions >o – ikh Aug 11 '14 at 11:25
  • 2
    You can get N3337, which was (I believe) the first draft after C++11, and only includes very, very minor changes [here](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf) for free. As for finding the proper part of the standard, it's usually a matter of judging the sections of the PDF, learning where things are (by looking things up/citing the standard), and searching for relevant keywords with CTRL+F. – Robert Allan Hennigan Leahy Aug 11 '14 at 11:28
  • 1
    @ikh [Where do I find the current C or C++ standard documents?](http://stackoverflow.com/questions/81656/where-do-i-find-the-current-c-or-c-standard-documents) is the best question for this and as far I know covers all the drafts that are available for both C and C++. – Shafik Yaghmour Aug 11 '14 at 11:49
  • 1
    I think something is missing from this answer: How is the overload resolution between two conversion functions performed wrt to the "return" type? `struct foo { operator int(); operator bool(); };` can unambiguously convert to `int`. See [over.match.best]/1.4, which is only applied *after* determining which conversion sequence is *better*. – dyp Aug 11 '14 at 11:51
  • The clause in §13.3.3 [over.match.best] about conversion sequences from return types is only considered if it is **not** the case that: "for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2)". But for the implicit `this` parameter, not adding `const` is better than adding `const`. See §13.3.3.2 [over.ics.rank]. – Robert Allan Hennigan Leahy Aug 11 '14 at 12:10
  • 2
    That's what I said with "is only applied after determining which conversion sequence is better". Yet, I think this is an important part of the answer: there's (in theory) a conflict between the conversion of the implied object argument and the "conversion of the return type", and it's resolved unambiguously by the different steps of overload resolution. [over.match.best]/1.4 clearly points out the separation of those steps, and why the final standard conversion sequence doesn't care in this case. – dyp Aug 11 '14 at 12:22
  • 2
    I edited the answer to incorporate your suggestion, elaborating on why the return type of these conversion operators is not considered when selecting the "best" one. – Robert Allan Hennigan Leahy Aug 11 '14 at 12:35
  • 1
    Sorry, but how does this resolve the difference between GCC/Clang and VS2013? Is VS2013 just wrong? – raptortech97 Aug 12 '14 at 11:53
  • 2
    @RobertAllanHenniganLeahy Since that was the point of the question, shouldn't it appear somewhere in the answer, not just a comment? – Barmar Aug 12 '14 at 20:49
  • @Barmar: The question is "[w]hich is the correct behavior according to the standard?" The answer states "`operator int` should be selected." I don't see the problem. – Robert Allan Hennigan Leahy Aug 13 '14 at 07:48
9

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.

Pixelchemist
  • 24,090
  • 7
  • 47
  • 71
  • In most cases, it's quite intuitive that what you do with the result doesn't affect how it's computed. How we compute `a/b` is the same whether the code is `float f = a/b;` or `int f = a/b;`. But this is a case where it can be a bit surprising. – David Schwartz Jul 03 '19 at 23:27
1

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.

Dr. Debasish Jana
  • 6,980
  • 4
  • 30
  • 69
  • 8
    It's a nice solution to write program, but it doesn't answer *why* this strange thing occurs in detail. – ikh Aug 11 '14 at 11:10
  • 4
    `true and false with corresponding values 1 and 0` That's an oversimplification. `true` converts to integer `1`, but that doesn't mean it "has" the value `1`. Indeed, `true` may be `42` underneath. – Lightness Races in Orbit Aug 11 '14 at 11:12
  • Having explicit bool operator in B2 avoids the confusion and compiler dependencies of what get called in B2, operator int or operator bool. – Dr. Debasish Jana Aug 11 '14 at 11:13
  • There is no compiler dependency. The Standard requires that 0 converts to false and 1 converts to true. – Puppy Aug 11 '14 at 11:14
  • 1
    @LightnessRacesinOrbit More to the point, perhaps: a `bool` never has the value 0 or 1; the only values it can take are `false` and `true` (which convert to 0 and 1 if the `bool` is converted to an integral type). – James Kanze Aug 11 '14 at 11:54
  • Your reasoning is incorrect: `int` doesn't dominate due to being in the derived class. It dominates due to being non-const as other answers describe. The same problem would occur if we just had `B` containing both operators. – M.M Dec 24 '14 at 00:14
0

Some of the previous answers, already provide a lot of info.

My contribution is, "cast operations" are compiled similar, to "overloaded operations", I suggest to make a function with a unique identifier for each operation, and later, replace it by the required operator or cast.

#include <iostream>

class B {
public:
    bool ToBool() const {
        return false;
    }
};

class B2 : public B {
public:
    int ToInt() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << b.ToBool() << std::endl;
}

And, later, apply the operator or cast.

#include <iostream>

class B {
public:
    operator bool() {
        return false;
    }
};

class B2 : public B {
public:
    operator int() {
        return 5;
    }
};

int main() {
    B2 b;
    std::cout << std::boolalpha << (bool)b << std::endl;
}

Just my 2 cents.

umlcat
  • 4,091
  • 3
  • 19
  • 29