Thread overview
Thinking about nothing: Solving the weirdness of the `void` type
Jul 14
Basile B.
July 13

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:

  1. Make void an alias to typeof(null) when used as a return type. This opens a transition path to remove void as a return type without (much) breakage. Almost all of the post is about this.
  2. Remove concessions needed so that Step 1 had little breakage.
  3. Finally, make void as a return type invalid and make void*, void[n] and void[] 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.

July 13
On 7/13/23 19:58, Quirin Schroll wrote:
> **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.
> ...

Some issues with that:

1. deprecation of language features has been deprecated

2. `void` is compatible with C and everyone has built muscle memory for typing it. Also, it is true that `void[]` is semantically completely unrelated to `void`, but it's still a special case of `T[]` for `T=void`.

3. `typeof(null)` is not a canonical unit type. It has a subtyping relationship with class references and pointers. This means you would sometimes actually need to reserve a full machine word for passing a `typeof(null)` across a virtual function boundary, while ideally a unit type can be passed without any dedicated code being executed.

```d
class C{
    void foo(typeof(null) x){}
}
class D:C{
    void foo(C c){}
}
```

There's probably more issues, this is just off the top of my head.
July 13

On Thursday, 13 July 2023 at 17:58:27 UTC, Quirin Schroll wrote:

>

Key idea: Make void an alias to typeof(null) when used as a return type, with the goal of deprecating void return types altogether.

This proposal seems strictly worse in every respect than Dennis's proposal to make void a unit type with a size of 1. It will break approximately 100% of D projects (even "Hello world" uses void main()), and in return we get...even more special cases than we started with? A type that can be used as an array member but not as a return value?

I appreciate the effort you've put into thinking this through, but, as the kids say, this ain't it.

July 14

On Thursday, 13 July 2023 at 18:56:06 UTC, Timon Gehr wrote:

>

On 7/13/23 19:58, Quirin Schroll wrote:

>

Key idea: Make void an alias to typeof(null) when used as a return type, with the goal of deprecating void return types altogether.

  1. typeof(null) is not a canonical unit type. It has a subtyping relationship with class references and pointers. This means you would sometimes actually need to reserve a full

That's correct but typeof(null) is actually more simply the type of the null expression 1.

Then by implicit conversion it indeed converts to all reference tpyes and pointers types.

That implicit cast happens 99.99% of the time, but not always e.g:

void* v()
{
    auto error() // the internal type null
    {
        things();
        return null;
    }
    if (auto s = stuff())
        return s;
    return error(); // cast(void*) error()
}

So the proposed change cannot work.