Demystifying C++ - Strict Return
In C and C++ programming, ensuring that functions behave predictably and efficiently is paramount. One particular scenario that often requires attention is when a non-void function fails to return a value on all control paths, potentially leading to undefined behavior and compromising the stability of the program. This situation is commonly flagged by compilers, which provide warnings to alert developers of potential issues in the code.
In the following example, a function with missing return
statement is translated. Surprisingly, even that the input code only uses C constructs, the semantic equivalent C code contains __builtin_unreachable
as additional function call. __builtin_unreachable
is an builtin function available in clang and GCC compiler that marks unreachable code. If control flow reaches the point of the __builtin_unreachable
, the program is undefined. It is useful in situations where the compiler cannot deduce the unreachability of the code. (see https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html).
Code with missing return | int func1(void* ptr); int func2(void* ptr) { if (ptr) return func1(ptr); // warning: non-void function does not // return a value in all control paths } |
int func1(void* ptr); int func2(void* ptr) { if (ptr) return func1(ptr); __builtin_unreachable(); } |
---|
The reason for this is that C++ and C have different requirements regarding missing return
within the standard. In C11 the standard says that
If the '}' that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined. (6.9.1p12)
That means a function with missing return
is only undefined behavior if the return value is used. In C++ the standard says that
Flowing off the end of a function [...] results in undefined behavior in a value-returning function. ([stmt.return]p2)
which means that in C++ any function with missing return
is undefined behavior independent if it is used or not. In contrast to C, the clang compiler marks the position within the source code as unreachable in case of a missing return
in C++. Within the compiler frontend, that behavior is called strict return. To mimic strict return
also in C, the __builtin_unreachable
must be added in the generated C source code.
However, marking code paths as unreachable by the compiler frontend in case of missing return
can lead to strange effects that every developer should be aware of. In the following code, the C code before and after optimization is shown:
After frontend (before optimization) | After optimization |
---|---|
int func1(void* ptr); int func2(void* ptr) { if (ptr) return func1(ptr); __builtin_unreachable(); } |
int func1(void* ptr); int func2(void* ptr) { return func1(ptr); } |
During optimization the compiler recognizes two code path. If the condition is true, the result of func1 is returned. Otherwise, the code is unreachable. As code that is unreachable should never be reached, the optimizers thinks that the condition is meaningless and optimizes it away. That means for an end users, that a missing return
cannot only lead to run-time errors at the location where the return
is missing but can even influence code before! This optimization is reproducible both for clang and gcc compiler.
For that reason, missing return
warnings should be taken seriously and either enable an error in that case with -Werror=return-type
or deactivate strict return
within the compiler using --no-strict-return
.