5

I am generally using clang to develop code, using all reasonable warnings I can (-Wall -Wextra [-Wpedantic]). One of the nice things about this setup is that the compiler checks for the consistency of the switch stataments in relation to the enumeration used. For example in this code:

enum class E{e1, e2};

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22; // if I forget this line, clang warns
    }
}

clang would complain (warn) if: I omit either the e1 or the e2 case, and there is no-default case.

<source>:4:12: warning: enumeration value 'e2' not handled in switch [-Wswitch]
    switch(e){

This behavior is great because

  1. it checks for consistency at compile time between enums and switches, making them a very useful and inseparable pair of features.
  2. I don't need to define an artificial default case for which I wouldn't have a good thing to do.
  3. It allows me to omit a global return for which I wouldn't have a good thing to return (sometimes the return is not a simple type like int, it could be a type without a default constructor for example.

(Note that I am using an enum class so I assume only valid cases, as an invalid case can only be generated by a nasty cast on the callers end.)

Now the bad news: Unfortunately this breaks down quickly when switching to other compilers. In GCC and Intel (icc) the above code warns (using the same flags) that I am not returning from a non-void function.

<source>: In function 'int fun(E)':
<source>:11:1: warning: control reaches end of non-void function [-Wreturn-type]
   11 | }
      | ^
Compiler returned: 0

The only solution I found for this working to both have a default case and return a non-sensical value.

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
        default: return {}; // or int{} // needed by GCC and icc
    }
}

This is bad because of the reasons I stated above (and not even getting to the case where the return type has no default constructor). But it is also bad because I can forget again one of the enum cases and now clang will not complain because there is a default case.

So what I ended up doing is to have this ugly code that works on these compilers and warns when it can for the right reasons.

enum E{e1, e2};

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
#ifndef __clang__    
        default: return {};
#endif
    }
}

or

int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
    }
#ifndef __clang__    
    return {};
#endif
}

Is there a better way to do this?

This is the example: https://godbolt.org/z/h5_HAs


In the case on non-default constructible classes I am really out of good options completely:

A fun(E e){
    switch(e){
        case E::e1: return A{11};
        case E::e2: return A{22};
    }
#ifndef __clang__
    return reinterpret_cast<A const&>(e);  // :P, because return A{} could be invalid
#endif
}

https://godbolt.org/z/3WC5v8

alfC
  • 14,261
  • 4
  • 67
  • 118

3 Answers3

5

It is important to note that, given your initial definition of fun, it is entirely legal C++ to do the following:

fun(static_cast<E>(2));

Any enumeration type can assume any value within the number of bits of its representation. The representation for a type with an explicit underlying type (enum class always has an underlying type; int by default) is the entirety of that underlying type. Therefore, an enum class by default can assume the value of any int.

This is not undefined behavior in C++.

As such, GCC is well within its rights to assume that fun may get any value within the range of its underlying type, rather than only one of its enumerators.

Standard C++ doesn't really have an answer for this. In an ideal world, C++ would have a contract system where you can declare up-front that fun requires that the parameter e be one of the enumerators. With that knowledge, GCC would know that the switch will take all control paths. Of course, even if C++20 had contracts (which is being retooled for C++23), there still isn't a way to test if an enum value only has values equal to one of its enumerators.

In a slightly less ideal world, C++ would have a way to explicitly tell the compiler that a piece of code is expected to be unreachable, and therefore the compiler can ignore the possibility of execution getting there. Unfortunately, that feature didn't make C++20 either.

So for the time being, you're stuck with compiler-specific alternatives.

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

All of these three compilers have the __builtin_unreachable() extension. You can use it to both suppress the warning (even if the return value has constructor problems) and to elicit better code generation:

enum class E{e1, e2};


int fun(E e){
    switch(e){
        case E::e1: return 11;
        case E::e2: return 22;
    }
    __builtin_unreachable();

}

https://godbolt.org/z/0VP9af

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
  • This is very useful. I can even put this after the closing curly to avoid extra lines. `... } __builtin_unreachable();`. – alfC Oct 30 '19 at 19:09
  • Not that I use MSVC but for reference MSVC doesn't have `__builtin_unreachable()` https://godbolt.org/z/1FRDEK – alfC Oct 30 '19 at 19:32
  • 1
    @alfC It's best to hide it behind a macro rather than use it directly, in case some compiler you might want to support doesn't have it. I use `*(char*)0=0` as my alternative `__builtin_unreachable()`. That doesn't help to silence the warning on msvc, but you could do something like `UNREACHABLE; return 11 /*anything valid*/;` and then the warning vanishes and the extra `return 11;` after `UNREACHABLE` doesn't make the code on gcc/clang/icc any worse: https://godbolt.org/z/pbcnWC. – Petr Skocik Oct 30 '19 at 19:54
  • Do you know why `(char*)0=0` can be an alternative for `unreachable`? – alfC Jun 12 '20 at 21:56
  • @alfC `*(char*)0=someInt` can't ever be executed or the program behavior is undefined. The only way a program with `*(char*)0=someInt` can be valid is if this statement is unreachable. So `*(char*)0=someInt` should have the same effect as `__builtin_unreachable()`. Practically on gcc/clang it doesn't, but this is unfortunate because if compilers did treat it that way, then the `__builtin_unreachable()` extension wouldn't be needed. – Petr Skocik Jun 12 '20 at 22:05
0

This has nothing to do with enum or switch and everything to do with the compiler’s ability to prove a valid return statement through every path. Some compilers are better at this than others.

The correct way is to just add a valid return at the end of the function.

A fun(E e){
  switch(c){
    case E::e1: return A{11};
    ...
  }
  return A{11}; // can't get here, so return anything
}

Edit: some compilers (like MSVC) will complain if you have a return from an unreachable path. Just bracket the return by an #if for the compiler. Or as I often do, just have a RETURN(x) defined that is defined based on the compiler.

Dúthomhas
  • 8,200
  • 2
  • 17
  • 39
  • I found that MSVC doesn't actually complain about that particularly: https://godbolt.org/z/ScGhhL – alfC Oct 30 '19 at 19:15