|
|
Log in / Subscribe / Register

Void is surprisingly useful.

Void is surprisingly useful.

Posted Sep 9, 2026 16:40 UTC (Wed) by NYKevin (subscriber, #129325)
In reply to: Void is surprisingly useful. by alx.manpages
Parent article: Stabilizing Rust's never type

The problem with optional type parameters, in generic code (which is the only kind of code that has "type parameters" of any kind, optional or not), is combinatorial blowup. For each optional type parameter, we must write twice as many implementations. This might be worth it, if we could accomplish something special with optional type parameters that is otherwise impossible. But it turns out that the overwhelming majority of cases look like one of the following:

* A user-supplied callback function might be void, so we need not propagate its return value.
* The user might only want to use part of our container (e.g. using a hash table as a hash set), so we need not store the rest.
* A user-supplied function might be infallible, so we need not handle errors.

In other words, the vast majority of cases just look like deleting some code that we don't need. So you don't really need optional type parameters, you just need types that tell the compiler to delete some code. That gives us () and !:

* () ("unit") means roughly "don't emit code to store or pass along values of this type." It's a type whose only value is always constant-folded out of existence. It's good for cases where we want the code to do basically the same thing, except without storing a value.
* ! ("never") means roughly "don't emit code in any scope where a value of this type is observed to exist." It's a type with no values, so you don't need any code to handle it. It's good for cases where we want some branch(es) to disappear entirely, e.g. for removing error handling or otherwise forcing compile-time evaluation of something.

! has the additional behavior of making some pattern matches infallible. If we have a Result<V, !>, it may be infallibly pattern-matched against Ok(v) because Rust knows the Err() case can't exist. This doesn't work with Result<V, ()>, because that means "there is an error case, but no error information is provided" (similar to Option<V>).


to post comments

Void is surprisingly useful.

Posted Sep 9, 2026 17:05 UTC (Wed) by iabervon (subscriber, #722) [Link]

Yeah, that's what I meant. And C started out thinking of types as "how do I interpret the bits in memory or registers" which doesn't encourage having () or ! as types, so void is kind of all of these plus an unsafe Any depending on where you used it instead of a regular type, for one of those use cases.

Void is surprisingly useful.

Posted Sep 9, 2026 21:15 UTC (Wed) by alx.manpages (subscriber, #145117) [Link] (5 responses)

The problem with optional type parameters, in generic code (which is the only kind of code that has "type parameters" of any kind, optional or not), is combinatorial blowup. For each optional type parameter, we must write twice as many implementations.

Not necessarily. Here's some generic code with a single implementation:

#define rvalue(lv)          ((void)0, (lv))
#define typeof_typename(T)  typeof(*(typeof(T) *){_Generic(0, T: NULL, default: NULL)})
#define mallocarray(...)    reallocarray(NULL, __VA_ARGS__)
#define malloc_T(n, T)      rvalue((typeof_typename(T) *){mallocarray(n, sizeof(T))})

malloc_T() is implemented internally through void* (through reallocarray(3)), but exposes a type-generic API. We could say reallocarray(3) is unsafe, but the exposed API of malloc_T() is safe. And we didn't need an exponential number of implementations. But it can certainly be used with hundreds of types.

int *p = malloc_T(42, int);
long *q = malloc_T(6, long);
struct foo *r = malloc_T(7, struct foo);

Am I misunderstanding something?

Void is surprisingly useful.

Posted Sep 10, 2026 0:19 UTC (Thu) by NYKevin (subscriber, #129325) [Link] (4 responses)

An "optional type parameter" is a type parameter that you can leave out at the callsite. In your example, that would mean an API that lets you write malloc_T(37, void) or some other syntax instead of "void", such as malloc_T(37). But you can't do that with your example, because sizeof(void) is nonsense. So your example has a mandatory type parameter, not an optional type parameter.

Void is surprisingly useful.

Posted Sep 10, 2026 9:52 UTC (Thu) by alx.manpages (subscriber, #145117) [Link] (3 responses)

Indeed, this API lets you write malloc_T(42, void). It's not allowed in the ISO C dialect, because sizeof(void) is not valid there. But in GNU C, sizeof(void)==1, and thus this is perfectly allowed.

alx@devuan:~/tmp$ cat malloc.c 
#include <stdlib.h>

#define rvalue(lv)          ((void)0, (lv))
#define typeof_typename(T)  typeof(*(typeof(T) *){_Generic(0, T: NULL, default: NULL)})
#define mallocarray(...)    reallocarray(NULL, __VA_ARGS__)
#define malloc_T(n, T)      rvalue((typeof_typename(T) *){mallocarray(n, sizeof(T))})

int
main(void)
{
	void *p = malloc_T(42, void);

	/*
	 * The memory in 'p' doesn't have an effective type yet at this
	 * point.  This is useful for example for creating a memory pool
	 * from which to use memory later.
	 */

	int *i = p;
	*i = 7;

	/*
	 * The first byte of the pool now has an effective type of
	 * 'int'.  The rest remains without effective type.
	 */

	free(p);
}
$ gcc -Wall -Wextra malloc.c 
$

Void is surprisingly useful.

Posted Sep 10, 2026 9:53 UTC (Thu) by alx.manpages (subscriber, #145117) [Link]

	/*
	 * The first byte of the pool now has an effective type of
	 * 'int'.  The rest remains without effective type.
	 */

Oh, well, I meant the first 4 bytes, of course. :)

Void is surprisingly useful.

Posted Sep 11, 2026 22:29 UTC (Fri) by NYKevin (subscriber, #129325) [Link] (1 responses)

Not sure I would characterize that as an "optional" type parameter. I would maybe call it "GNU C decided to make void act more like a real type, and less like the absence of a type." Anyway, if you're going to give examples in GNU C, you should specify. ISO is the standard dialect (GCC's out-of-the-box behavior notwithstanding).

Of course, there has to be some allowance for special cases where you don't need multiple implementations, because you could always write a function that does not actually use its type parameter. But my point is that in the general case, where you may want to write arbitrary code, you do face a combinatorial explosion. There might happen to be a few special cases where it's not strictly necessary... but Rust in particular is not a fan of this sort of thing.

In Rust (unlike C++), generic definitions have to be valid for all possible choices of parameters, not just for the particular invocation(s) that happen to appear in your project. This is essential for backwards compatibility. If someone calls into your function in version 1.0 with a given set of generic parameters, then that same set of parameters ought to work in version 1.1 (assuming you follow semver or something like semver). Rust upholds that guarantee without forcing you to test all possible (or all "reasonable") combinations of parameters manually, because it does type checking before the generics are monomorphized. By the time parameters are actually substituted, we already know that the resulting code will compile. And you know that you're backwards compatible as long as you didn't change the signature (which includes information about which types are "allowed" to be used as type parameters).

So now you can see the issue: If we want to allow for "optional" type parameters in Rust, we need to fit them into the type checking scheme. At that point, it is a lot easier to say "OK, we're promoting these empty values to full types and giving them completely regular type-level semantics" than it is to say "OK, we're going to litter the generic type logic with dozens of special cases scattered all over the place just in case a type parameter turns out to be empty."

Void is surprisingly useful.

Posted Sep 11, 2026 23:25 UTC (Fri) by alx.manpages (subscriber, #145117) [Link]

> Anyway, if you're going to give examples in GNU C, you should specify. ISO is the standard dialect (GCC's out-of-the-box behavior notwithstanding).

I say it now. ISO C is like speaking standard Latin: nobody does it, and nobody ever did (except maybe a few Romans, during a few years, and maybe neither). It's good as a guideline on which to base dialects, not as an actual dialect meant to be directly useful. Very few programs (none that are non-trivial?) are strictly conforming to ISO C (and even less to ISO C90).

> But my point is that in the general case, where you may want to write arbitrary code, you do face a combinatorial explosion. There might happen to be a few special cases where it's not strictly necessary...

I write a lot of type-generic code using GNU C23, and only have a few places where I've had to duplicate code; they are the special case, IME. We're working to improve the language in ways that those special cases may not be needed in the future.

> but Rust in particular is not a fan of this sort of thing.

I don't write any Rust, but this seems consistent with what I've heard, and with the little Rust I've read: it tends to be more verbose than C.


Copyright © 2026, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds