|
|
Log in / Subscribe / Register

Void is surprisingly useful.

Void is surprisingly useful.

Posted Sep 9, 2026 11:47 UTC (Wed) by ralfj (subscriber, #172874)
In reply to: Void is surprisingly useful. by dcoutts
Parent article: Stabilizing Rust's never type

Note that Haskell's Void and C's void are very different types. If we map them both to Rust, Haskell's Void is like ! (the empty type) and C's void is like () (the unit type). The fact that C's void is often described as a "type with no values" is extremely misleading and confusing: C's void has exactly one value, and because there is only one value you don't have to spell it out and it encodes 0 bits of information. In Rust if a function has return type () you also don't need to spell out its return value, but you can if you want -- the syntax for the only value of the unit type is () as well.

"void" sounds like it should be an empty type, so IMO it is very unfortunate that C picked this term.


to post comments

Void is surprisingly useful.

Posted Sep 9, 2026 12:50 UTC (Wed) by tux3 (subscriber, #101245) [Link] (15 responses)

Also note that C's rules around void are too irregular to make it work as a unit type. You can't declare a void variable. It's not really an empty type or a unit type, it's a secret third thing.

In C++ this forces templates to special-case void. There was a proposal to make void more regular, but that wasn't going to happen without breakage, so C++ added std::monostate instead, which is actually intended as a unit type.

Void is surprisingly useful.

Posted Sep 9, 2026 12:54 UTC (Wed) by ralfj (subscriber, #172874) [Link] (14 responses)

I would say `void` is a poorly supported accidental unit type in C. ;)

Void is surprisingly useful.

Posted Sep 9, 2026 13:42 UTC (Wed) by iabervon (subscriber, #722) [Link] (13 responses)

I think it's not really a type at all; it's a keyword used in places in type declarations where an optional type parameter isn't used. While "int *" is "pointer to int", "void *" isn't "pointer to void", it's just "pointer". "int (*)(int)" is "function that takes int and returns int", but "void (*)(int)" is "function that takes int and calling it is a statement rather than an expression", and "int (*)(void)" is "function that returns int". Subsequent language design research showed that optional type parameters are a bad idea, and you can make your parameterized types uniform by including some types that the compiler optimizes (like having "function that returns a value you must discard" instead), but that's not how C was designed, so void ends up with all the weirdness that you get if you try to have a value that embodies the effects of not using something optional.

Void is surprisingly useful.

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

"void (*)(int)" is "function that takes int and calling it is a statement rather than an expression"

void functions still produce expressions. This can be seen in the comma operator:

void f(void);

42, f(), 42;

exit() returns void, but it's still usable as an expression. You just can't assign it to something, because variables of type void are not allowed. But that doesn't make them statements.

Void is surprisingly useful.

Posted Sep 9, 2026 16:17 UTC (Wed) by NYKevin (subscriber, #129325) [Link]

I think a better way of expressing it might be "void must be discarded."

(I was going to say "void can't be an rvalue," but your example is indeed an rvalue!)

Void is surprisingly useful.

Posted Sep 13, 2026 9:35 UTC (Sun) by koflerdavid (subscriber, #176408) [Link] (1 responses)

Thanks for showing this, I didn't know this compiles.

Void is surprisingly useful.

Posted Sep 13, 2026 11:31 UTC (Sun) by alx.manpages (subscriber, #145117) [Link]

Another --more interesting-- example is with the ternary operator:

#define assert(e)  ((e) ? (void)0 : abort())

Void is surprisingly useful.

Posted Sep 9, 2026 14:55 UTC (Wed) by alx.manpages (subscriber, #145117) [Link] (8 responses)

> Subsequent language design research showed that optional type parameters are a bad idea,

I'm not sure I understand this. Would you mind rephrasing? Or maybe (or also) showing examples?

> and you can make your parameterized types uniform by including some types that the compiler optimizes (like having "function that returns a value you must discard" instead), but that's not how C was designed, so void ends up with all the weirdness that you get if you try to have a value that embodies the effects of not using something optional.

Void is surprisingly useful.

Posted Sep 9, 2026 16:40 UTC (Wed) by NYKevin (subscriber, #129325) [Link] (7 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. 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>).

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.

Regular void

Posted Sep 10, 2026 9:57 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (6 responses)

In C++ this idea that their void should be a unit type often comes up as "Regular void" to distinguish this from the present status that void is irregular because it is treated as "incomplete" in their language whereas a pointer to void is an actual type.

Here's an example of a "Regular void" proposal for C++
https://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p014...

But while "regular void" is superficially attractive, it can't really work in C or C++ and that proposal illustrates some of the difficulty and I'm sure responses when it was presented get into some others.

Basically C has a crap type system and C++ mostly makes things worse rather than better, a "regular void" might be a reasonable patch in a good type system, but in C++ it just substitutes one set of crazy edge cases for a different set of crazy edge cases.

Regular void

Posted Sep 10, 2026 10:24 UTC (Thu) by ralfj (subscriber, #172874) [Link] (4 responses)

Fixing such an early mistake in a language with a huge installed base is indeed hard. Rust made the right call with () (our void equivalent) from the start, but we made ! not-a-type initially and it took us 10 years and I don't even remember how many attempts to fix that...

Regular void

Posted Sep 10, 2026 11:05 UTC (Thu) by tialaramex (subscriber, #21167) [Link] (3 responses)

I used to have a lot more sympathy for the "It's hard to fix" people before I saw how hard WG21 worked to shoot down Epochs (an attempt to give C++ a feature like Rust's editions, after seeing it work just once in 2018 Edition) and Sean's "Safe C++" work and read JeanHeyd's lengthy battle to land #embed in C at WG14

These are not people who tried hard to improve the language but were foiled by how difficult it was to deliver because the language is very old, they're people who didn't want to change and found every excuse possible for why everything must remain exactly as it is. Computer Science is a young discipline, and I'm an old man, but this sort of thing is how we get that saying "Science advances one funeral at a time".

Regular void

Posted Sep 11, 2026 17:49 UTC (Fri) by wahern (subscriber, #37304) [Link] (2 responses)

Well, #embed has already had to undergo revision shortly after standardization in C23: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3912.htm Even the simplest features can be tricky.

Granted, the headaches come from C wanting to support some degree of source-level compatibility with C++, as well as having many implementations, including multiple *major* implementations. These are headaches languages like Rust don't have to deal with. An implementation quirk in Rust can just become the de facto standard, or supported as hack through backward compatibility features. But when you have multiple implementations that diverge, there's no tidy way to fix that mess.

Regular void

Posted Sep 11, 2026 18:58 UTC (Fri) by mb (subscriber, #50428) [Link] (1 responses)

>wanting to support some degree of source-level compatibility with C++

Not having an FFI between C++ and C is a major problem.
Having it would free C++ from many of the compatibility problems.

It is fixable by introducing it in a new standard version.
But there does not seem to be willingness to do it - I presume - as it would complicate programs that mix C and C++ willy-nilly

Regular void

Posted Sep 11, 2026 22:51 UTC (Fri) by wahern (subscriber, #37304) [Link]

Almost certainly will never happen. My impression is the committee avoids name mangling like the plague. And name mangling aside, I assume there's a host of other thorny issues regarding parameter passing (e.g. references, implicit this, etc) that nobody wants to tackle. Is there any prior art? I don't remember GCC or clang having any extensions to help calling into non-extern-C C++.

Regular void

Posted Sep 11, 2026 22:04 UTC (Fri) by magfr (subscriber, #16052) [Link]

When I read this I thought it looked more like the proposed bottom type of P3549R0.


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