The naming scheme for “void” always confused me when comparing:
C/C++ inspired languages and their ilk
more functional-inspired* languages.
In C (etc.), void means “doesn’t return anything”, which is different from “doesn’t return at all”.
In many functional languages, void is the bottom type, meaning a function returning it “doesn’t return at all”. Their functions usually return the “unit type” for the “doesn’t return anything” case.
Where did this divide come from?
* funnily enough, python implicitly returns None from a function that reaches its end without a return. And within python, NoneType is the only builtin that could be considered a “unit type”! I’m sure there are other similar examples.
Interesting. I hadn’t realised that there’s a point to [[noreturn]] int fn() (the thing that we don’t return is an int), but I can see that you may well want to allow a function to slot into a code path that will set up a call frame expecting a specific return type. I still find it a bit confusing that [[noreturn]] functions are allowed to return via exception throwing. You can also write [[noreturn]] int fn() noexcept, which isn’t allowed to return via any mechanism and which frees the compiler to destroy any non-escaped caller state at the call site.
The lack of participation in the type system is a bit sad, because it means that you can’t guarantee that a function pointer is noreturn. C++17 changed this for noexcept and that leads to some interesting interaction with std::function, which doesn’t have overloads for noexcept functions because it must be able to handle the case where it is uninitialised. You could imagine a non-standard version that required the return type to be default constructable and was initialised to point to a function that returned a default-initialised value, which would not have this constraint.
My mental model for a function not returning is that no code on the stack above that invocation is ever reachable. In particular, the compiler is free to trash anything on the stack in setting up the call frame and is free to not bother emitting any code to handle anything after the call instruction. Without noexcept, most of this is not true for a C++ [[noreturn]] function. I’m actually curious what the standard says if you longjmp out of a [[noreturn]] function and whether noexcept makes a difference.
Returning void means you return no value. [[noreturn]] means you don’t return at all. ie, put a debugger breakpoint right before the function’s } and it will never get triggered.
The naming scheme for “void” always confused me when comparing:
In C (etc.),
void
means “doesn’t return anything”, which is different from “doesn’t return at all”.In many functional languages,
void
is the bottom type, meaning a function returning it “doesn’t return at all”. Their functions usually return the “unit type” for the “doesn’t return anything” case.Where did this divide come from?
* funnily enough, python implicitly returns
None
from a function that reaches its end without areturn
. And within python,NoneType
is the only builtin that could be considered a “unit type”! I’m sure there are other similar examples.Interesting. I hadn’t realised that there’s a point to
[[noreturn]] int fn()
(the thing that we don’t return is anint
), but I can see that you may well want to allow a function to slot into a code path that will set up a call frame expecting a specific return type. I still find it a bit confusing that[[noreturn]]
functions are allowed to return via exception throwing. You can also write[[noreturn]] int fn() noexcept
, which isn’t allowed to return via any mechanism and which frees the compiler to destroy any non-escaped caller state at the call site.The lack of participation in the type system is a bit sad, because it means that you can’t guarantee that a function pointer is noreturn. C++17 changed this for
noexcept
and that leads to some interesting interaction withstd::function
, which doesn’t have overloads fornoexcept
functions because it must be able to handle the case where it is uninitialised. You could imagine a non-standard version that required the return type to be default constructable and was initialised to point to a function that returned a default-initialised value, which would not have this constraint.I never thought of throwing an exception as “returning”. Do you think of a function
longjmp
’ing as also “returning”?FYI, C++23 added
std::move_only_function
which does have specializations fornoexcept
.My mental model for a function not returning is that no code on the stack above that invocation is ever reachable. In particular, the compiler is free to trash anything on the stack in setting up the call frame and is free to not bother emitting any code to handle anything after the call instruction. Without
noexcept
, most of this is not true for a C++[[noreturn]]
function. I’m actually curious what the standard says if youlongjmp
out of a[[noreturn]]
function and whethernoexcept
makes a difference.As of a few versions, you can just use the return type “noreturn” in D. https://twitter.com/walterbright/status/1364135799369789441
Returning
void
means you return no value.[[noreturn]]
means you don’t return at all. ie, put a debugger breakpoint right before the function’s}
and it will never get triggered.