Demystifying C++ - Strict Return

From emmtrix Wiki
Jump to navigation Jump to search


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.