return with no argumentWe wish to support the following use cases:
(). A return value must always
be produced.C++ treats void as a special case in a number of ways. We want to minimize the
impact of this special-case treatment for the corresponding Carbon types. One
way that this special treatment is visible in C++ is that functions with the
return type void obey different rules: the operand of return becomes
optional (but is still permitted), and reaching the end of the function becomes
equivalent to return;. In a function template, this can even happen invisibly,
with some instantiations having an implicit return; and others not.
This interferes with the ability to reason about programs. Consider:
template<typename T> auto f() {
if (T::cond())
return T::run();
}
Here, it is possible for control flow to reach the end of the function. However,
a compiler can't warn on this without false positives, because it's possible
that T::run() has type void, in which case this function has an implicit
return; added before its }, and indeed, it might be the case that T::run()
always has return type void for any T where T::cond() returns false.
See the following issues:
return statementsVoid and ()?return; in functions with a Void return type?Instead of applying special-case rules based on whether the return type of a
function is (), we apply special-case rules based on whether a return type is
provided.
A function with no declared return type is a procedure. The return type of a
procedure is implicitly (), and a procedure always returns the value () if
and when it returns. Inside a procedure, return must have no argument, and if
control flow reaches the end of a procedure, the behavior is as if return; is
executed.
// F is a procedure.
fn F() {
if (cond) {
return;
}
if (cond2) {
// Error: cannot return a value from a procedure.
return F();
}
// Implicitly `return;` here.
}
A function with a declared return type is treated uniformly regardless of
whether that return type happens to be (). Every return statement must
return a value. There is no implicit return at the end of the function, and
instead a compile error is produced if control flow can reach the end of the
function, even if the return type is () -- or any other unit type.
fn G() -> () {
if (cond) {
// OK, F() returns ().
return F();
}
if (cond2) {
// Error: return without value in value-returning function.
return;
}
// Error: control flow can reach end of value-returning function.
}
From the caller's perspective, there is no difference between a function declared as
fn DoTheThing() -> ();
and a function declared as
fn DoTheThing();
As a result, the choice to include or omit the -> () in the definition is an
implementation detail, and the syntax used in a forward declaration is not
required to match that used in a definition. The use of -> () in idiomatic
Carbon code is expected to be rare, but it is permitted for uniformity, and in
case there is a reason to desire the value-returning-function rules or to
emphasize to the reader that () is the return type or similar.
Issue #510 asks whether we should support named return variables:
fn F() -> var ReturnType: x {
// initialize x
return;
}
If we do, functions using that syntax should follow the rules for procedures in
this proposal, including the implicit return; if control reaches the end of
the function. In particular,
fn F() { ... }
would be exactly equivalent to
fn F() -> var (): _ = () { ... }
return Procedure();. This in turn may make it easier to change a
return type from () to something else, but this proposal by itself is
insufficient to ensure that is always possible.() or not, and symmetrically by using different
syntax for different semantics.return F(); being mixed with
return; in the same function.return in a function
be determined based on syntax alone, we permit checks for missing
return statements to be provided in the definition of a template or
generic, without needing to know the arguments. This is important for
generics in particular, because we do not want monomorphization to be
able to fail and because we do not in general guarantee that
monomorphization will be performed.return statement can be detected while
parsing, using only syntactic information rather than contextual,
semantic information. In practice, we will likely parse both kinds of
return statement in all functions and check the return type from a
context that has the semantic information, but the ability to do these
checks syntactically may be useful for simple tools and editor
integration.This proposal rejects some constructs that would be valid in C++:
return F();
in a function with void return type would no longer be valid in a
corresponding Carbon function with no specified return type, and would
need to be translated into
F();
return;
(possibly with braces added). However, the fact that this construct is valid in C++ is surprising to many, and the constructs that would be idiomatic in C++ are still valid under these rules.
The advantages of this approach compared to maintaining the C++ rule are
discussed above. The advantage of maintaining the C++ rule would be that Carbon
is more closely aligned with C++. However, the removed functionality --
specifically, the ability to return an expression of type void from a void
returning function -- is still available, albeit with a more verbose syntax, and
the existence of that functionality in C++ is a source of surprise to C++
programmers.
We could treat the choice of function with () return type versus procedure as
being part of the interface rather than being an implementation detail.
// F is a procedure.
fn F();
// F is a function returning ().
fn G() -> ();
// ...
// Error, procedure redeclared as a function.
fn F() -> () {
return ();
}
// Error, function redeclared as a procedure.
fn G() {
}
Then, we could disallow any use of a procedure call in a context that depends on its return value, treating a procedure call as a statement rather than as an expression that can be used as a subexpression or an operand of an operator.
fn Func() -> ();
fn Proc();
// OK, x is of type ().
auto x = Func();
// Error, Proc is a procedure.
auto y = Proc();
Advantages:
() in this context. Procedures no longer
need to say that their return type is implicitly () nor that they
implicitly return ().() value is stored
and used.
().Disadvantages:
auto x = F(); regardless of whether F is a function or
procedure may be important for generic code.