17

Why do switch and if statements behave differently with conversion operators?

struct WrapperA
{
    explicit operator bool() { return false; }    
};

struct WrapperB
{
    explicit operator int() { return 0; }
};

int main()
{
    WrapperA wrapper_a;
    if (wrapper_a) { /** this line compiles **/ }

    WrapperB wrapper_b;
    switch (wrapper_b) { /** this line does NOT compile **/ }
}

The compilation error is switch quantity is not an integer while in the if statement it is perfectly recognized as a bool. (GCC)

Martin Schröder
  • 4,176
  • 7
  • 47
  • 81
nyarlathotep108
  • 5,275
  • 2
  • 26
  • 64

6 Answers6

17

The syntax is switch ( condition ) statement with

condition - any expression of integral or enumeration type, or of a class type contextually implicitly convertible to an integral or enumeration type, or a declaration of a single non-array variable of such type with a brace-or-equals initializer.

Taken from cppreference.

This means you can only do a switch case on an integer or enum type. For the compiler to be able implicitly convert Wrapper to integer / enum type you need to remove the explicit keyword :

The explicit specifier specifies that a constructor or conversion function (since C++11) doesn't allow implicit conversions

You can also cast Wrapper to int type.

Edit to adress @acraig5075 remarks :

You must be careful which operator is explicit and which is implicit. If both are implicit the code won't compile because there will be an amibiguity :

struct Wrapper
{
    operator int() { return 0; }
    operator bool() { return true; }    
};

source_file.cpp: In function ‘int main()’: source_file.cpp:12:14:

error: ambiguous default type conversion from ‘Wrapper’

switch (w) {

^ source_file.cpp:12:14: note: candidate conversion

include ‘Wrapper::operator int()’ and ‘Wrapper::operator bool()’

The only way to remove the ambiguity is to do a cast.

If only one of the operator is explicit, the other one will be chosen for the switch statement :

#include <iostream>
struct Wrapper
{
    explicit operator int() { return 0; }
    operator bool() { return true; }    
};

int main()
{
    Wrapper w;
    if (w) { /** this line compiles **/std::cout << " if is true " << std::endl; }
    switch (w) { 
        case 0:
            std::cout << "case 0" << std::endl;
            break;
        case 1:
            std::cout << "case 1" << std::endl;
            break;
    }
    return 0;
}

Output :

 if is true 
case 1

w has been implicitly converted to 1 (true) (because operator int is explicit) and case 1 is executed.

On the other hand :

struct Wrapper
{
    operator int() { return 0; }
    explicit operator bool() { return true; }    
};

Ouput :

 if is true 
case 0

w has been implicitly converted to 0 because operator bool is explicit.

In both case, the if statement is true because w is evaluated contextually to a boolean inside the if-statement.

Community
  • 1
  • 1
Clonk
  • 2,025
  • 1
  • 11
  • 26
  • 2
    But there'll still be an error of ambiguity if both the bool and int casts are present, even if both are not declared explicit. – acraig5075 Jun 06 '18 at 12:45
  • I added an example to adress the ambiguity part – Clonk Jun 06 '18 at 13:04
  • So is the 2 cent summary that `switch` does an implicit conversion to int, whereas `if` does an explicit conversion (to bool)? – R.M. Jun 06 '18 at 15:15
  • Basically, yes. The compiler cast the condition inside a if-statement to a boolean. This is why there is no ambiguity with `if(...)` even if both are implicit operator. – Clonk Jun 06 '18 at 15:19
11

I think this explains why the switch statement is not accepted, whereas the if statement is:

In the following five contexts, the type bool is expected and the implicit conversion sequence is built if the declaration bool t(e); is well-formed. that is, the explicit user-defined conversion function such as explicit T::operator bool() const; is considered. Such expression e is said to be contextually convertible to bool.

  • controlling expression of if, while, for;
  • the logical operators !, && and ||;
  • the conditional operator ?:;
  • static_assert;
  • noexcept.
Marco Luzzara
  • 5,540
  • 3
  • 16
  • 42
  • This explains why `if` is accepted, but does not explain why the same concept of "contextually convertible" has been decided to not apply towards an integral type when inside the context of a `switch` statement. – nyarlathotep108 Jun 06 '18 at 12:49
  • 3
    Go on reading when it refers to the 3 context behind (included the `switch`): `Note that explicit conversion functions are not considered, even though they are considered in contextual conversions to bool.` However I did not found a precise reason why this is not implemented with `switch`. – Marco Luzzara Jun 06 '18 at 13:36
  • 2
    The "double standard" here (so to say) is puzzling me. – nyarlathotep108 Jun 06 '18 at 13:45
  • 1
    @nyarlathotep108: `explicit` conversion operators exist to prevent people from performing implicit conversions. The "contextual conversion to `bool`" rule exists for the sole purpose of excepting this rule in certain very specific locations. It only works for `bool`, and only in those locations, by design. This allows us to avoid needing the "safe bool" idiom, while still being able to have types that are convertible to `bool` for conditions. – Nicol Bolas Jun 06 '18 at 16:29
  • @nyarlathotep108 there's nothing else in C++ that offers an automatic/implicit cast to any integral type. Consider a class that contains three cast operators returning different enum types, which of the three should the implicit switch cast prefer? – arnt Jun 13 '18 at 09:05
3

One answer is that if and switch behave differently because that's how the standard was written. Another answer might speculate on why the standard was written that way. Well, I suppose the standard made if statements behave that way to address a specific problem (implicit conversions to bool had been problematic), but I'd like to adopt a different perspective.

In an if statement, the condition must be a boolean value. Not everyone thinks about if statements this way, presumably because of the various conveniences built into the language. However, at its core, an if statement needs to know "do this" or "do that"; "yes" or "no"; true or false -- i.e. a boolean value. In this respect, putting something inside the statement's conditional is explicitly requesting that the something be converted to bool.

On the other hand, a switch statement accepts any integral type. That is, there is no single type preferred over all others. Use of a switch can be seen as an explicit request to convert a value to an integral type, but not necessarily specifically to int. So it is not considered appropriate to use a conversion to int when that specific conversion needs to be explicitly requested.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
3

Declaring a conversion operator explicit exists to prevent implicit conversions to that type. That is its purpose. switch attempts to implicitly convert its argument to an integer; therefore an explicit operator will not be called. That's the expected behavior.

What is unexpected is that an explicit operator is called in the if case. And thereby hangs a tale.

See, given the above rules, the way to make a type testable via if is to make a non-explicit conversion to bool. The thing is... bool is a problematic type. It is implicitly convertible to integers. So if you make a type that is implicitly convertible to bool, this is legal code:

void foo(int);
foo(convertible_type{});

But this is also meaningless code. You never meant for convertible_type to implicitly convert to an integer. You just wanted it to convert to a boolean for testing purposes.

Pre-C++11, the way to fix that was the "safe bool idiom", which was a complex pain and made no logical sense (basically, you provided an implicit conversion to a member pointer, which was convertible to a boolean but not to integers or regular pointers).

So in C++11, when they added explicit conversion operators, they made an exception for bool. If you have an explicit operator bool(), this type can be "contextually converted to bool" inside of a set number of language-defined places. This allows explicit operator bool() to mean "testable in boolean conditions".

switch doesn't need such protection. If you want a type to be implicitly convertible to int for switch purposes, there's no reason why it wouldn't be implicitly convertible to int for other purposes too.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
1

I believe the real reason for this behavior has its roots in C, but before explaining that, I'll try to justify it in C++ terms.

An if/while/for statement is supposed to take any scalar (integer, float, or pointer), or class instance convertible to a bool. It simply checks to see if the value is equivalent to zero. Given this permissiveness, it's relatively harmless for the compiler to use an explicit operator to fit a value into an if statement.

switch, on the other hand, only really makes sense with integers and enums. If you try to use switch with a double or pointer, you get the same error. This makes it easier to define discrete case values. Since a switch statement specifically needs to see an integer, it's probably a mistake to use a class instance that doesn't define implicit conversion.

Historically, the reason is that this is how C does it.

C++ was originally intended to be backward compatible with C (though it's never been successful at that). C never had a boolean type until more recent times. C's if statements had to be permissive, because there just wasn't any other way to do it. While C does not do struct-to-scalar type conversions like C++ does, C++'s treatment of explicit methods reflects the fact that if statements are very permissive in both languages.

C's switch statement, unlike if, needs to work with discrete values, therefore it can't be so permissive.

luther
  • 5,195
  • 1
  • 14
  • 24
  • This answer is inaccurate concerning C. In C, an `if` statement's conditional must be a scalar type (not a class instance -- C has no concept of "convertible"). So this particular question, involving a struct convertible to a scalar type, is not applicable in C; i.e. C++ can handle it any way it likes and still maintain compatibility with C. No matter what is done, it would not be how C does it. In fact, the use of explicit conversions was not even how C++ handled `if` statements until C++11. – JaMiT Jun 07 '18 at 02:33
  • I did not say that C does conversions in `if` statements. I'm trying to explain why the behavior described in the question is exactly what one would expect given how floats and pointers are treated in both languages. C is applicable here because that's the language that set the precedent. As someone who learned C before C++, it seems obvious to me that this is the reasoning that the OP is looking for. – luther Jun 07 '18 at 04:40
  • The word "obvious" is overused. For example, the behavior is not what **I** would expect given how floats and pointers are treated in both languages. As someone who learned C before C++, it seems to me that these conclusions are wrong, hence *not* what the OP is looking for. – JaMiT Jun 07 '18 at 06:43
  • To clarify: the behavior for `if` statements is not what I would expect. If talking about just the behavior of `switch` statements, then sure, it's the same in both languages. But that does not really address the question of why `if` looks at explicit conversions but `switch` does not. – JaMiT Jun 07 '18 at 06:49
  • Another clarification: I have no issues with the rationale for why this behavior is reasonable. It's the (boldface) assertion that "this is how C does it" that I am questioning. From my perspective, this answer would be fine if the mentions of C were dropped. – JaMiT Jun 07 '18 at 07:04
  • I added a sentence to try to clarify the connection. I understand that people can disagree on what's obvious. It happens all the time. But when someone asks why a language feature is the way it is, I think it's important to dig as far as we need to to show how that feature fits into the larger scheme of things. Even if the exact feature in the question doesn't exist in C, we still have to talk about how C sets the precedent for `if` statements being as permissive as they are. – luther Jun 07 '18 at 12:14
0

There are two problems with your code. First, the conversion operator(s) must not be explicit to work with switch statements. Secondly, switch condition requires an integral type and both int and bool are such types so there is an ambiguity. If you change your class so that it doesn't have these two problems the switch will compile and work as expected.

Another solution that doesn't require changing your class is to explicitly cast (static_cast will do) the value to int or bool.

navyblue
  • 776
  • 4
  • 8
  • I am reformulating the question as the example code is raising ambuigity on the purpose of the question itself – nyarlathotep108 Jun 06 '18 at 13:11
  • 1
    After you've reformulated the question: Regarding `switch`, there is no ambiguity but the conversion operator is explicit so it won't work because `switch` requires that the condition expression is implicitly convertible to an integral type. When it comes to `if`, its condition is such a context that explicit conversion to `bool` is also taken into account by the compiler so it's fine. In other words, `switch` requires an integral type and only implicit conversions may be used while in `if` statement a `bool` is required but both implicit and explicit conversions may be used. – navyblue Jun 06 '18 at 13:48