"control reaches end of non-void function" with fully handled case switch over an enum type
Asked Answered
H

3

28

Why does this code trigger a "control reaches end of non-void function" even if all possible values of type_t are handled? What is the best way to take care of this warning? Adding a return -1 after the switch?
(Code tested here)

typedef enum {
    A,
    B
} type_t;

int useType(type_t x) {
    switch (x) {
        case A:
            return 0;
        case B:
            return 1;
    }
}


Related: Detecting if casting an int to an enum results into a non-enumerated value
Hawkbill answered 9/11, 2015 at 10:37 Comment(6)
What if I do useType(2); ?Ritualism
or use default: with your preferred assert mechanism.Chromato
I wonder what to do about this kind of double questions. Will answers that only answer one question have any chance to be accepted? Is it up to the asker to decide or may the community intervene if they accept an answer for only one of the questions? Or should the questions be split into sparate posts?Gonna
May be of interest #6032319Gonna
@ᐅJohannesSchaub-litbᐊ I don't think this is a double question, it is in fact pretty typical: What is the cause of this problem? How do I solve it?Hawkbill
Pethaps you can put an infinite loop into a default label. The compiler should understand that it doesnt continue there.Gonna
R
17

In general, enums are not exclusive. Someone could call your function like useType( (type_t)3 ); for example. This is mentioned specifically in C++14 [dcl.enum]/8:

It is possible to define an enumeration that has values not defined by any of its enumerators.

Now, there are a bunch of rules about exactly which other values are possible for which other sorts of enum.

There are two categories of enum. The first is fixed underlying type, e.g. enum type_t : int, or enum class type_t . In those cases all values of the underlying type are valid enumerators.

The second is not fixed underlying type, which includes pre-C++11 enums such as yours. In this case the rule about values can be summarized by saying: compute the smallest number of bits necessary in order to store all values of the enum; then any number expressible in that number of bits is a valid value.


So - in your specific case, a single bit can hold both values A and B, so 3 is not valid value for the enumerator.

But if your enum were A,B,C, then even though 3 is not listed specifically, it is a valid value by the above rule. (So we can see that almost all enums will not be exclusive).

Now we need to look at the rule for what happens if someone does actually try to convert 3 to type_t. The conversion rule is C++14 [expr.static.cast]/10, which says that an unspecified value is produced.

However, CWG issue 1766 recognized that the C++14 text was defective and has replaced it with the following:

A value of integral or enumeration type can be explicitly converted to a complete enumeration type. The value is unchanged if the original value is within the range of the enumeration values (7.2). Otherwise, the behavior is undefined.

Therefore, in your specific case of exactly two enumerators with value 0 and 1, no other value is possible unless the program has already triggered undefined behaviour, so the warning could be considered a false positive.


To remove the warning, add a default: case that does something. I'd also suggest, in the interest of defensive programming, that it's a good idea to have a default case anyway. In practice it may serve to 'contain' the undefined behaviour: if someone does happen to pass an invalid value then you can throw or abort cleanly.


NB: Regarding the warning itself: it's impossible for a compiler to accurately warn if and only if control flow would reach the end of a function , because this would require solving the halting problem.

They tend to err on the side of caution: the compiler will warn if it is not completely sure, meaning that there are false positives.

So the presence of this warning does not necessarily indicate that the executable would actually allow entry into the default path.

Radiolocation answered 9/11, 2015 at 10:41 Comment(8)
Were they designed that way or was this just a mistake? This could have been UB (well, not now, backwards compatibility...)Chromato
Can an enum be made exclusive?Hawkbill
How? :) (I think that would answer the second part of my question, "What is the best way to take care of this warning?")Hawkbill
I've started a language lawyer question to try and clear up that last quoteRadiolocation
@Radiolocation But then what is the best/most elegant way to get rid of this false positive?Hawkbill
As far as I can see, you answered the first of the double question: Why does this code trigger a "control reaches end of non-void function" even if all possible values of type_t are handled?, but you left unanswered the second one: What is the best way to take care of this warning? Adding a return -1 after the switch?Gonna
@Hawkbill updated question with latest information. Your enum is actually exclusive and the warning is a false positive. I've added a section suggesting a way of avoiding the warning.Radiolocation
Adding a default label makes Clang complain: default label in switch which covers all enumeration values. Sometimes you just can't win...Worriment
S
4

To answer the second question ("What is the best way to take care of this warning?"):

In my eyes, typically, the best method is to add a call to __builtin_unreachable() after the switch statement (available both in GCC and Clang - maybe at some point we'll get [[unreachable]]). This way, you explicitly tell the compiler that the code never runs over the switch statement. And if it does, you are happy to accept all dire consequences of undefined behaviour. Note that the most obvious reason to run over would be an enum containing a value that is not listed - which is undefined behaviour anyway as pointed out in the answer by @M.M.

This way, you get rid of the warning on current versions of both GCC and Clang without introducing new warnings. What you lose is the protection by the compiler if you miss a valid situation that does run over the switch statement. This is mitigated to some degree that both GCC and Clang do warn you if miss a switch case completely (e.g. if a value gets added to the enum), but not if one of the cases runs into a break statement.

Satiable answered 24/6, 2020 at 19:13 Comment(2)
And if it does, you are happy to accept all dire consequences of undefined behaviour. In an age where undefined behavior can literally format your hard drive (bugs.llvm.org/show_bug.cgi?id=49599), I think this really understates the possible consequences. Although it might seem safe in conjunction with -Werror=switch, in a codebase that undergoes a lot of refactoring, the [[unreachable]] can quickly end up far away from the original switch statement. I would therefore advocate for a controlled assertion failure instead, or, if code size is a concern, something like __debugbreak.Lionellionello
According to cppreference, C++23 will have a std::unreachable function.Lashonda
R
0

I had the same issue with C and gcc 11.4, I find that an assert(false); after the switch suppresses the warning (so avoiding compiler-specific extensions).

Ronnieronny answered 5/9, 2023 at 13:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.