|
|
Log in / Subscribe / Register

Void is surprisingly useful.

Void is surprisingly useful.

Posted Sep 9, 2026 21:15 UTC (Wed) by alx.manpages (subscriber, #145117)
In reply to: Void is surprisingly useful. by NYKevin
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.

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?


to post comments

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