Without form and void
void is a pretty strange thing. We can cast expressions to
void, throw-expressions have
void* can point to anything, and
void functions can actually return back to the caller. But we can’t have objects of type
void or even write a type like
void&. A function declaration like
void f(void) is actually a nullary function. It’s a bit weird — but it’s not something a lot of people lose sleep over.
Until it starts wreaking havoc on your generic code — because it’s like the
vector<bool> of the type system.
A really common first run-in with the kinds of problems that
void can cause is when you try to write that first benchmarking function (personally, my first non-trivial case was a result of fulfilling promises). All you want to do is time some arbitrary function and log how long it takes. No problem:
This basically does what we want. We take two timestamps around invoking our function (using
std::invoke because of course pointers to members aren’t callable…), log the delta in units of seconds, and just return the
result back to the caller. And this works for all functions (that return a moveable type) except those that return
void — which give us a compile error pointing to the
result declaration with:
error: 'void result' has incomplete type
auto result = std::invoke(/* ... */)
So, what do we do now? One solution that I’m immediately rejecting is to provide an overload of
timeit() for those functions that return
void, duplicating the entire body. This is only mildly frustrating in this case (it’s only a 10 line function after all), but it’s a complete non-starter in general — I am not going to duplicate every function template I write that accepts arbitrary callables.
One clever solution to this particular problem (and I am using “clever” here in its typical connotation as being complimentary) is to reorganize the function such that all the work after the call gets thrown into a scope guard:
This works. And if you think about it, it’s kind of strange that it does work. Well, first, there’s the putting all of this work in a scope guard to take advantage of the fact that local variables’ (such as the one implicitly created by this macro) destructors are sequenced after the return statement, and so this does in fact time the function. That’s… okay, maybe that’s a little strange. But the real strange part here is the fact that this very much looks like we’re returning an object of type
void, which is what caused us problems in our first version. The language doesn’t really work in that way, there is no
void object, we just get a special rule that says that this works for
But it’s only a matter of time in which this doesn’t quite work. We can’t quite reorganize each and every function to just return expressions of type
void. What do we do then?
My personal preferred approach is to introduce a type as a stand-in for
void that can be freely substituted back and forth, and provide helpers that make it easy to switch between
void and this new type as necessary. For lack of a better name, I call this type…
Void. It’s a pretty simple type:
Empty type, explicitly constructible from anything. Pretty basic. On top of this, we add two type aliases to convert between
Void in the type system, if so desired:
Now, how do we use this thing? The main pain point of
void is dealing with unknown callables that might return it. The solution is to provide a function template that does the same thing as
std::invoke(), but just returns
Void instead of
void — the advantage here being that
Void is an actual object type.
Without Concepts, that’s two overloads that are mutually SFINAE-d out, one for the
void case and one for the non-
The existence of these function templates allows us to just swap in
invoke_void() wherever we had previously used
std::invoke(). Our initial example, for instance, rewritten to handle
void return types would be:
This just works, and we don’t need to know about the scope guard trick nor have to worry about writing duplicate templates going forward.
But there’s one more little thing that I’ve found very useful that’s probably more controversial.
My implementation of
optional<T>, in addition to supporting reference types, also supports
void (which internally is treated as
Void, I do not allow
optional<Void>). You might wonder that
optional<T&> is just an odd spelling of
T*, but come on —
optional<void> is a particularly ridiculous spelling of
bool. And… well, kind of. But it makes it easy in generic contexts if there’s just no holes in what
optional supports. This implementation also has all the member function that Simon Brand proposes in [P0798].
The question is, how should
optional<void>::map() behave? What kind of callable should it take? There are two options here:
- Since internally it’s a
map()should take a unary function that takes a
Voidargument, with the appropriate cv-ref qualifiers.
- Since externally it’s
void, and you cannot have objects of type
map()should take a nullary function.
- No seriously, why do you have
I think option 2 is the best choice here. It just seems like the most useful choice, that’s the least surprising for callers. And so, to avoid fragmenting the templates again, I actually have three overloads of
invoke_void(), not two:
In other words,
invoke_void(f, Void()) is equivalent to
invoke_void(f) for all
f (that are actually nullary). This is true even if you try to call
invoke_void with a function that can actually take a
Void as an argument. It just won’t happen. I haven’t run into a case where I’ve actually wanted that though…
The only reason this 3rd overload exists is to make my life easier with all the monadic functions. For all I’m aware, it has no sound backing in programming language theory. But I can tell you with certainty that it’s been super useful and I would write it again.
Of course, this comes back to the question of — why is
void so weird anyway? The answer to all of those questions is usually something to the effect of “Because, C.” Although on occasion, it’s some earlier language’s fault. I don’t know the history here, but would be curious to find out.
In any case, wouldn’t all of this just be simpler if we didn’t have to work around
void to begin with? What if
void were, you know, just a regular type?
Void only exists because
void isn’t an object type. But the one little quirk remains that invoking a function with an instance of
void, if it were a real type, would still have to behave as if it were a real argument. That is, consider:
void were a real type, this shouldn’t compile. In my world, currently, it does. I don’t really know what to make of this situation, but I’m sure I know a person or two who has a strong opinion on the subject.