Search code examples
c#c++language-design

Why does C++ allow functions that don't actually return a value?


In C++, a function with a non-void return type without a return statement is allowed. So, the following code will compile:

std::string give_me_a_string()
{
}

In C#, however, such a method is not allowed. So, the following code will not compile:

public string GiveMeAString()
{
}

Why is this the case? What was the design rationale in these two languages?


Solution

  • C++ requires code to be "well-behaved" in order to be executed in a defined manner, but the language doesn't try to be smarter than the programmer – when a situation arises that could lead to undefined behaviour, the compiler is free to assume that such a situation can actually never happen at runtime, even though it cannot be proved via its static analysis.

    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.

    Calling such a function is a legitimate action; only flowing off its end without providing a value is undefined. I'd say there are legitimate (and mostly legacy) reasons for permitting this, for example you might be calling a function that always throws an exception or performs longjmp (or does so conditionally but you know it always happens in this place, and [[noreturn]] only came in C++11).

    This is a double-edged sword though, as while not having to provide a value in a situation you know cannot happen can be advantageous to further optimization of the code, you could also omit it by mistake, akin to reading from an uninitialized variable. There have been lots of mistakes like this in the past, so that's why modern compilers warn you about this, and sometimes also insert guards that make this somewhat manageable at runtime.

    As an illustration, an overly optimizing compiler could assume that a function that never produces its return value actually never returns, and it could proceed with this reasoning up to the point of creating an empty main method instead of your code.


    C#, on the other hand, has different design principles. It is meant to be compiled to intermediate code, not native code, and thus its definability rules must comply with the rules of the intermediate code. And CIL must be verifiable in order to be executed in some places, so a situation like flowing off the end of a function must be detected beforehand.

    Another principle of C# is disallowing undefined behaviour in common cases. Since it is also younger than C++, it has the advantage of assuming computers are efficient enough to support more powerful static analysis than what the situation was during the beginning of C++. The compilers can afford detecting this situation, and since the CIL has to be verifiable, only two actions were viable: silently emit code that throws an exception (sort of assert false), or disallow this completely. Since C# also had the advantage of learning from C++'s lessons, the developers chose the latter option.

    This still has its drawbacks – there are helper methods that are made to never return, and there is still no way to statically represent this in the language, so you have to use something like return default; after calling such methods, potentially confusing anyone who reads the code.