5

I did some research to find out why a missing return cannot be an error but is undefined behavior instead. I found this comment in a bug report that uses the following example to illustrate why it cannot be an error:

   template<typename T>
   T maybe_call(std::function<T(void)> f) {
           if (f)
                   return f();
           else
                   abort_program();

           // Cannot write a return here, because we have no way to create a value of t
   }

Note that this example is completely valid code. It has no return on all branches, but still there is no UB. That explains why not having a return on all branches is UB, not an error. The example could be "fixed" to declare abort_program(); as [[noreturn]] to make it less of a counter argument. I doubt that compilers would have problems to correctly diagnose this example. If missing a return would be turned into an error, rules would maybe have to change a bit, because only with the [[noreturn]] the above example can be diagnosed correctly.

However, what I am looking for is a different example where the compiler cannot detect a missing return. That comment also mentions that cases exists that cannot be diagnosed by the compiler, but I fail to find such an example.

If the warning would be reliable (false positives like the one above aside) I could treat the warning as error to be on the safe side.

Are there really cases where a compiler cannot detect a missing return? Is it only in pathological code, or can it happen in every day code too? Can I rely on the warning?

To make it answerable, lets concentrate on gcc(latest version): In what case gcc fails to warn for a missing return?

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 2
    If the `abort_program` function is marked as `noreturn` (either using a compiler-specific attribute or the C++ standard [`[[noreturn]]`](https://en.cppreference.com/w/cpp/language/attributes/noreturn) attribute) then the compiler should be able to handle it. – Some programmer dude Apr 01 '20 at 08:49
  • Otherwise, for optional return-values you could use [`std::optional`](https://en.cppreference.com/w/cpp/utility/optional) (or [the Boost equivalent](https://www.boost.org/doc/libs/1_72_0/libs/optional/doc/html/index.html)). – Some programmer dude Apr 01 '20 at 08:50
  • @Someprogrammerdude good point, but also without `[[noreturn]]` the example is valid code. For your second comment: the question isnt so much about the example I included but about an example that has a missing return but no warning is triggered. I belive gcc has no problem to warn about that example. Sorry if that isnt as clear as it could be – 463035818_is_not_an_ai Apr 01 '20 at 08:52
  • warning is a valid diagnostics which was required. – Swift - Friday Pie Apr 01 '20 at 08:57
  • @Swift-FridayPie not sure if I understand your comment. Afaik missing return is undefined behavior no diagnostic required. – 463035818_is_not_an_ai Apr 01 '20 at 09:02
  • In your example, if `f()` throws an exception in some circumstances and returns a value in all other circumstances, that function cannot be marked as `[[noreturn]]`, but the code will be correct and well-defined even if `maybe_call()` does not have a `return` after the `if`/`else`. Also, before C++11, it was not possible to mark functions that way, and the undefined behaviour would only occur if the caller *used* the return value - so a caller that discarded the return value would not have undefined behaviour. – Peter Apr 01 '20 at 09:06
  • with `if (0 <= i) return i; else throw_if_negative(i);`, later function cannot be marked as `[[no_return]]`. – Jarod42 Apr 01 '20 at 09:07
  • @Jarod42 true, the more I think about it I get convinced that the problem is not that the compiler cannot detect that a `return` is missing but false positives. Also if the function is not marked as `[[noreturn]]` the compiler would have to figure out it `abort_program();` eventually returns which is the halting problem – 463035818_is_not_an_ai Apr 01 '20 at 09:12
  • @Peter no it is not required that the caller uses the return value. It is UB rightaway. Jonathan Wakely had the same misunderstanding in the bug thread – 463035818_is_not_an_ai Apr 01 '20 at 09:15
  • @idclev463035818 - Before C++11, that was not true. – Peter Apr 01 '20 at 10:03
  • @Peter I dont think so. This is from C++03: "Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function" – 463035818_is_not_an_ai Apr 01 '20 at 10:20
  • When you mentioned `abort_program();` you meant the same as `abort()` function action of C++ ? – Alan Nunes Apr 01 '20 at 15:19

2 Answers2

1

It is difficult to prove the non-existence of something, but having a hard time to find an example is convincing enough to see that only pathological code would cause the compiler to not warn about a missing return.

First my line of thinking was to construct a convoluted example along the line of:

int fun() {
    goto exit;
    return 1;
    exit: ;      
}

However, no matter how hard I try, gcc will warn about such code. The more I think about it, the more I get convinced that the problem is not that there can be code where the compiler will not warn, but rather false postives. If the example in the question is changed to

   template<typename T>
   T maybe_call(std::function<T(void)> f) {
           if (f)
                   return f();
           else
                   maybe_abort_program();

           // Cannot write a return here, because we have no way to create a value of t
   }

Then it would be up to the compiler to decide whether maybe_abort_program eventually returns and that is the halting problem which cannot be decided in all cases. Changing the name of the function of course changes nothing, but it is just to highlight that even thought that function could be marked as [[noreturn]], not marking it as such the code can still be valid (or not) and the compiler cannot reliably diagnose that.

However, compilers can warn about such cases. And after experimenting a bit I did not find any example where the compiler does not warn. Hence my conclusion is: I can (to some extend) rely on the warning.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • The `goto` example isn't convoluted, it's just `while (true) { return 1; }`. Trivially examinable by the compiler. [`goto` isn't a magic concoction that somehow breaks C++](https://stackoverflow.com/a/7334968/4386278), and the same rules that ensure this are the same rules that mean any `goto` chain convoluted enough to "break" code path analysis in the compiler are not legally permitted by the language. – Asteroids With Wings Apr 01 '20 at 11:19
  • @AsteroidsWithWings I didnt write that this code is convoluted. I wrote that I tried to come up with a convoluted code along the line of the code that I included. Also I know that `goto` will not immediately break C++, but I was hoping to use `goto` to get the example I was looking for, turned out i was wrong – 463035818_is_not_an_ai Apr 01 '20 at 11:22
  • Yes, you wrote, _"my line of thinking was to construct a convoluted example along the line of:"_, and that's what I was referring to... I'm confirming that you were wrong about `goto` and providing you with more information as to _why_ that is. – Asteroids With Wings Apr 01 '20 at 11:24
  • 1
    No problem idclev! – Asteroids With Wings Apr 01 '20 at 11:26
1

There is no reason for any such case to exist.

Every code path in a function in a well-defined program should contain only data manipulation (which cannot terminate the function's execution), internal flow (loops etc that are self-contained in the function by definition), return statements which end the function and throws/longjmps/exits/aborts which also end the function. These are all trivially visible to the compiler.

Other than that, it's just function calls, to which the same applies recursively.

The example/bug you mention here is literally the opposite: getting the warning because you didn't write a return, and you're relying on a called function to perform a throw/longjmp/exit/abort, which the compiler may not know about, and your function would have undefined behaviour if the called function doesn't perform that… and you did not use a compiler intrinsic to signal that your code path is "unreachable". Usually I much prefer the intrinsic to turning off -Werror; it can even give you performance benefits!

Asteroids With Wings
  • 17,071
  • 2
  • 21
  • 35