6

When I compile the following code at c++11 standard, it works fine with clang, and also with gcc, but gcc (all versions that I tested 4.8.2, 4.9.2, 5.1.0) gives a warning:

#include <iostream>

enum class FOO { A, B, C };

const char * bar(FOO f) {
  switch (f) {
    case FOO::A:
      return "A";
    case FOO::B:
      return "B";
    case FOO::C:
      return "C";
  }
}

int main() {
  unsigned int x;
  std::cin >> x;

  FOO f = static_cast<FOO>(x % 3);
  std::cout << bar(f) << std::endl;
}

The warning is -Wreturn-type:

main.cpp: In function ‘const char* bar(FOO)’:
main.cpp:14:1: error: control reaches end of non-void function [-Werror=return-type]
 }
 ^
cc1plus: all warnings being treated as errors

I still get the warning even with -O2 or -O3 optimizations -- does this mean that even at high optimization levels, gcc cannot dead-code eliminate the 'end' of the function?

Notably, it doesn't give me the warning about unhandled switch cases.

Edit: From experiments with godbolt, it appears that even at high levels, it doesn't dead code eliminate that. I'm not sure whether it could or if clang does for instance.

Is there a good way to suppress this warning locally within such a function, or is the only way to suppress this to disable the warning generally?

Edit: I guess the question poses a natural language lawyer question, judging from the answers:

Can a conforming compiler dead-code eliminate the "end" of the bar function in my listing? (Or 101010's version with return nullptr; appended?) Or does conforming to the standard require that it generate code to handle enum values that aren't part of the enum definition?

My belief was that it could dead-code eliminate that but you are welcome to prove me wrong.

Chris Beck
  • 15,614
  • 4
  • 51
  • 87
  • The problem is that `bar` is declared to return a `char *` but it doesn't in the `default` case. – Kenney Dec 06 '15 at 00:15
  • Are you so sure about that? The standard specifically allows you to not return anything from main. Adding `return 0;` at the end doesn't stop the warning, and the warning refers to the `bar` function explicitly anyways. – Chris Beck Dec 06 '15 at 00:16
  • You're right, sorry, it's just the bar function. – Kenney Dec 06 '15 at 00:17

4 Answers4

8

does this mean that even at high optimization levels, gcc cannot dead-code eliminate the 'end' of the function?

Yes, because it's not dead code.

The standard allows you to call your function with static_cast<FOO>(static_cast<int>(FOO::B) | static_cast<int>(FOO::C)) as the argument. Your switch doesn't handle this.

The reason you don't get a warning about this not being handled in your switch is because the warning would have way too many false positives.

  • `static_cast(static_cast(FOO::B) | static_cast(FOO::C))` [is undefined behavior](http://stackoverflow.com/q/18195312/3425536). – Emil Laine Dec 06 '15 at 00:36
  • 1
    @zenith Nope, `FOO` has a fixed underlying type `int`, so "the values of the enumeration are the values of the underlying type", and it's well defined to cast everything between `INT_MIN` and `INT_MAX` to `FOO`. – T.C. Dec 06 '15 at 00:39
  • 3
    It would be defined even if `FOO` didn't have a fixed underlying type. The range of *any* enum, with or without fixed underlying type, is enough to cover bitwise OR of any enumeration values. –  Dec 06 '15 at 00:44
  • And @zenith, you might want to read the answer you link to, especially the end bit. I'm not contradicting that answer at all, in fact it specifically calls out that an enumeration type without a fixed underlying type, that can hold values 1 and 2, can necessarily also hold value 3. –  Dec 06 '15 at 00:47
2

I would add an assert(false) at the end of the function:

#include <cassert>

const char * bar(FOO f) {
  switch (f) {
    case FOO::A:
      return "A";
    case FOO::B:
      return "B";
    case FOO::C:
      return "C";
  }

  assert(false);
}

(Note that adding it as the default case wouldn't be as good, since that would silence all unhandled switch case warnings when e.g. adding new enum values.)

This is also good for debugging because it lets you instantly know when bar receives an invalid argument for some reason.

Emil Laine
  • 41,598
  • 9
  • 101
  • 157
  • zenith: i mean what I really want is a *compile time* warning that a switch case is unhandled. and GCC indeed does this, by issuing the switch warning when I didn't do that. So I don't know why I need to add a *run-time* check for the error, if GCC is already verifying at compile time that I handled it correctly? – Chris Beck Dec 06 '15 at 00:27
  • But that compile-time check doesn't work anyway. Consider e.g. `FOO f = static_cast(x % 4);` (4 instead of 3) – Emil Laine Dec 06 '15 at 00:28
  • I thought that was UB anyways? – Chris Beck Dec 06 '15 at 00:28
  • Yes. But wouldn't you like to (possibly) catch that UB at runtime? – Emil Laine Dec 06 '15 at 00:30
  • I'd rather have all my switch functions like this have however many fewer unnecessary assembly statements in them, so that my emscripten app is that much smaller and loads that much faster. However, I played around in godbolt and it looks like even at O3 levels, it doesn't eliminate these statements, so I guess I can't get everything I want :) – Chris Beck Dec 06 '15 at 00:35
  • Sounds a bit like premature optimization, but I won't judge. – Emil Laine Dec 06 '15 at 00:42
  • Also, you can disable `assert`s at any time by defining `NDEBUG` before including `cassert`, so generating unnecessary code shouldn't be a problem. Doing this will both eliminate the warning and behave as if the `assert` statement wasn't there at all. (+ you get the advantage to runtime debugging when things go wrong) – Emil Laine Dec 06 '15 at 00:45
1

In order to suppress the warning change your code to:

const char * bar(FOO f) {
  switch (f) {
    case FOO::A:
      return "A";
    case FOO::B:
      return "B";
    case FOO::C:
      return "C";
  }

  return nullptr;
  ^^^^^^^^^^^^^^^
}

Or:

const char * bar(FOO f) {
  switch (f) {
    case FOO::A:
      return "A";
    case FOO::B:
      return "B";
    case FOO::C:
      return "C";
    default:
      return nullptr;
      ^^^^^^^^^^^^^^^
  }
}

The compiler warns you that you're not returning anything in the case where f doesn't fall to one of the cases provided in switch. Falling of the end of a non-void function without returning introduces undefined behavior.

the end of your bar function is not unreachable. If I alter the code to:

#include <iostream>

enum class FOO { A, B, C };

const char * bar(FOO f) {
  switch (f) {
    case FOO::A:
      return "A";
    case FOO::B:
      return "B";
    case FOO::C:
      return "C";
  }

  std::cout << "End Reached" << std::endl;
  //return nullptr;
}

int main() {
  unsigned int x = 10;
  FOO f = static_cast<FOO>(x);
  std::cout << bar(f) << std::endl;
}

Output:

End Reached

Live Demo

101010
  • 41,839
  • 11
  • 94
  • 168
  • ok, I get that, but more generally do you know why this is necessary? I mean gcc will just dead-code eliminate that return anyways, right? "The compiler warns that you're not returning anything in the case where f doesn't fall to one of the cases provided in switch." The compiler knows that there is no such case, right? Otherwise it would give me a different warning about that – Chris Beck Dec 06 '15 at 00:17
  • 1
    Dead code is something else entirely. This is about what is returned. Dead code is `if (0) { /*dead code */}`. – Kenney Dec 06 '15 at 00:18
  • @Kenney: I don't think so. Dead code is about eliminating unreachable statements. `return nullptr;` is unreachable so it shouldn't matter whether I write it or not. – Chris Beck Dec 06 '15 at 00:20
  • The `/*dead code*/` represents unreachable statements. `return nullptr;` is not unreachable: `f` can be something other than `FOO::A`, `FOO::B` or `FOO::C` - the `default` case I mentioned. – Kenney Dec 06 '15 at 00:22
  • @Kenney: obviously, there are no such other values and the compiler knows this, or it issues a different warning about unhandled switch cases. You can try deleting the `FOO::B` line to see what happens. So the compiler knows that one of the switch blocks is entered every time. Maybe it's not able to aggregate the information that each of those blocks returns, and conclude that we never emerge from switch block without returning, but that would be surprising to me. – Chris Beck Dec 06 '15 at 00:25
  • @Kenney: I guess you turned out to be right here, my apologies – Chris Beck Dec 06 '15 at 01:01
  • @Chris Thank you, very kind of you. I agree that in theory the compiler knows and should optimize it out, where it not for `static_cast` and other oddities... It's the same in Java. It surprised me too, but in the end I just trusted the compiler to be right. – Kenney Dec 06 '15 at 01:20
1

Surely there are more than one "best way". This answer approaches the problem by eliminating switch whatsoever.

...and this particular way might be not the one you expect. I'm risking to be downvoted, but I'm still posting this. The bottom line is: sometimes "paradigm shift" solution works better in the given application than any workarounds and warning disabling.

switch is a well-known "code smell". I don't say it's always bad, but sometimes you can do better than switch, and it worth to consider alternatives.

In our case, bar function does translation (in linguistic sense). It looks up for some key in the dictionary and returns a translated word. You could explicit your code by using standard containers. The classic example is std::map (which is called "dictionary" in other programming languages):

#include <map>

enum class FOO {
    A, B, C
};

// Might be a vector or even static array if your enumeration is contiguous
std::map<FOO, std::string> dict = {
    { FOO::A, "A" }, // maps ints to strings
    { FOO::B, "B" },
    { FOO::C, "C" },
};

int main() {
    auto it = dict.find(FOO::A);
    if (it == dict.cend()) {
        // Handle "no such key" case
    }
    auto a = it->second; // "A"
}

No matter which solution you choose, the problem arises if you don't have some key in the dictionary. You should handle this case anyway.

Ivan Aksamentov - Drop
  • 12,860
  • 3
  • 34
  • 61