Key idea: Make void
an alias to typeof(null)
when used as a return type, with the goal of deprecating void
return types altogether.
If this works, we have a three-step plan to transition the language into a state in which void
has a clear meaning: It is the invalid type, the not-a-type type as in the not-a-number floating-point number.
Another neat thing about this, the following steps are not needed for the previous ones to make sense.
Here are the steps:
- Make
void
an alias totypeof(null)
when used as a return type. This opens a transition path to removevoid
as a return type without (much) breakage. Almost all of the post is about this. - Remove concessions needed so that Step 1 had little breakage.
- Finally, make
void
as a return type invalid and makevoid*
,void[n]
andvoid[]
basic types.
Step 1 brings the language in a position so that for the breakage that is intentional in Step 2, there is a transition path. The language after Step 1 admits mixing “new and good” code with “old and bad” code. The last step is essentially closing the door to the past.
After Step 3, void*
, void[n]
and void[]
need the special-casing that they deserve. You might think that a template like
auto f(T)(T[] values);
should work fine with void[]
, but that depends on the template. Generally speaking, it’s likely that if the algorithm f
implements special-cases void[]
or (unintentionally) does not work with it anyways. The simple reason is that there are loads of things one can do with T[]
(and T
values) if and only if T
isn’t void
.
I’m not suggesting we change the syntax of void[]
to reflect that it’s not a slice of void
(one reason is that it’s additional and unnecessary breakage and another is that a void[]
still is a slice, void[n]
still is an array and void*
still is a pointer, at last sort of), but I do suggest that one cannot create the void[]
type by stitching together void
and []
. In the example above, I suggest one cannot instantiate f!void
. (If f
wants to support void[]
, the author should supply an overload.) This is exactly like you cannot stitch together void initialization: One has to use the void
keyword:
alias V = void;
int x = V; // error: type `void` has no value
We all know that void
is weird. Depending on usage, it’s not even a type, e.g. in void initialization where it serves as a keyword; this makes it weirder than in C++, but that actually isn’t the problem.
The problem is that the void
is weird as a type. You can have void*
, void[n]
and void[]
and you can seemingly return a void
object or get one via a function call, but you can’t have a void
typed local variable or parameter.
For the first two, there’s a “fix”: Consider void*
, void[n]
and void[]
as basic types (not as per the grammar, just conceptually) since really a void[]
(or void[n]
) isn’t a slice/array of actual void
objects.
The void
return is a different beast. The language kind of pretends that void
values exist when it comes to return
and function calling, e.g. this works:
void f();
void g() { return f(); }
This is already a concession to the design of void
. The code would, of course, work for int
(in place of void
) because int
does have values, but – maybe surprisingly – it also works for noreturn
, which does not have values. The problem is not the supposed concession, it’s the concession’s limitations: The difference between void
and any other type is that the transformation of going through a variable works for any type except void
:
void f();
void g()
{
auto x = f(); // type `void` is inferred from initializer `f()`, and variables cannot be of type `void`
return x; // cannot return non-void from `void` function
}
Part of the plan is to fix this weirdness without special-casing void
even more.
Note: Because typeof(null)
appears quite often in the remainder of this post, I’ll assume null_t
is an alias of typeof(null)
.
As a return type, void
is a unit-type. There were proposals to give void
unit-type semantics, but that’s not possible, because then void[]
and friends (especially unintentionally formed) would break. It occurred to me: With null_t
, doesn’t D already have perfectly good unit type, one whose slices aren’t anything special (apart from being quite useless), one that admits being the type of a parameter, local variable, data member, etc.? So, why not use it?
My idea was to make void
, when in the place of a return type, an alias for null_t
. Ideally, that’s it. Maybe we need to make concessions and find places where void
return types shouldn’t be an alias of null_t
. An example that came to my mind immediately is the explicit and implicit drop-out-of-function return
statement: If the return type is void
, a function returns implicitly at the end of its scope and a return statement can be without value. For null_t
, maybe this should not be allowed. And vice-versa, return null;
maybe shouldn’t be allowed for a void
function. On the other hand, there’s no real the damage to just allowing all of them.
null_t f() { } // Allow it?
null_t f() { return; } // Allow it?
null_t f() { return null; } // Definitely good!
void f() { } // Definitely good!
void f() { return; } // Definitely good!
void f() { return null; } // Allow it?
I don’t know if (and where) “return type void
actually is null_t
” runs into real-world issues, but maybe void
could become a type that represents “not really a valid type” as in: What’s the common type between int[]
and bool
? It’s void
, i.e. there is none; almost like pun of the “numbers” in float
/double
that are “not-a-number,” void
is a type that’s “not-a-type.”
We do have the issue that templates query things like is(typeof(f()) == void)
and these must continue to work. My best attempt would be to make void
be an alias of null_t
in this context as well, that is, special-case the is
query to interpret the pattern is(typeof(CallExpression) == void)
as if it were is(typeof(CallExpression) == null_t)
; this can be done because after making void
return types an alias of null_t
there really isn’t a way for a well-formed CallExpression to return actual void
. To test for mere well-formedness, one uses is(typeof())
without equality check (currently and with this change as well).
If we made typeof(f())
result in void
if the function is specified with void
, but then there would be (subtle) differences between specifying void
and null_t
as the return type, and this is something that we should really avoid.