Without form and void

Barry Revzin
5 min readMay 2, 2018

--

In C++, void is a pretty strange thing. We can cast expressions to void, throw-expressions have void type, 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 void.

Cool.

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 and 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-void case:

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:

  1. Since internally it’s a Void, map() should take a unary function that takes a Void argument, with the appropriate cv-ref qualifiers.
  2. Since externally it’s void, and you cannot have objects of type void, map() should take a nullary function.
  3. No seriously, why do you have optional<void>?!

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:

If 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.

--

--