23

void is a bizarre wart in the C++ type system. It's an incomplete type that cannot be completed, and it has all sort of magic rules about the restricted ways it can be employed:

A type cv void is an incomplete type that cannot be completed; such a type has an empty set of values. It is used as the return type for functions that do not return a value. Any expression can be explicitly converted to type cv void ([expr.cast]). An expression of type cv void shall be used only as an expression statement, as an operand of a comma expression, as a second or third operand of ?: ([expr.cond]), as the operand of typeid, noexcept, or decltype, as the expression in a return statement for a function with the return type cv void, or as the operand of an explicit conversion to type cv void.

(N4778, [basic.fundamental] ¶9)

Besides the itchy feeling about all those strange rules, due to the limited ways it can be used it often comes up as a painful special case when writing templates; most often it feels like we would like it to behave more like std::monostate.


Let's imagine for a moment that instead of the quotation above, the standard said about void something like

It's a type with definition equivalent to:

struct void {
    void()=default;
    template<typename T> explicit void(T &&) {}; // to allow cast to void
};

while keeping the void * magic - can alias any object, data pointers must survive the roundtrip through void *.

This:

  • should cover the existing use cases of the void type "proper";
  • could probably allow the removal of a decent amount of junk about it spread through the standard - e.g. [expr.cond] ¶2 would probably be unneeded, and [stmt.return] would be greatly simplified (while still keeping the "exception" that return with no expression is allowed for void and that "flowing off" of a void function is equivalent to return;);
  • should still be just as efficient - empty class optimization is nowadays supported everywhere;
  • be intrinsically compatible on modern ABIs, and could be still special-cased by the compiler on older ones.

Besides being compatible, this would provide:

  • construction, copy and move of those empty objects, eliminating the special cases generally needed in templates;
  • bonus pointer arithmetic on void *, operating as for char *, which is a common extension, quite useful when manipulating binary buffers.

Now, besides the possibly altered return values of <type_traits> stuff, what could this possibly break in code that is well-formed according to current (C++17) rules?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
  • 4
    Have you heard about [Regular Void](https://wg21.link/p0146)? – Rakete1111 Nov 07 '18 at 20:31
  • 1
    @Rakete1111: agh this _did_ seem like an obvious solution! Thank you, I'll look into it! – Matteo Italia Nov 07 '18 at 20:32
  • 3
    @Rakete1111 Arrays of `void` look like a killer feature. – user7860670 Nov 07 '18 at 20:43
  • 1
    The standard requires that `sizeof(void*) == sizeof(char*)`. The fact that pointers to incomplete types are a thing means that all struct pointers "smell the same". Since you are proposing that `void` be a struct, this means that `sizeof(void*) == sizeof(struct Foo*)`, which means that `sizeof(char*) == sizeof(struct Foo*)`, which makes life difficult for non-byte-addressable systems like TOPS-20. – Raymond Chen Nov 10 '18 at 04:52
  • @RaymondChen: is the problem you are highlighting that, given that `sizeof(void *) >= sizeof(any other data object)`, it would make `struct` pointers too big for no good reason? – Matteo Italia Nov 10 '18 at 10:55
  • @MatteoItalia It would force `sizeof(struct Foo*)` to be the same size as `sizeof(void*)`, and require implementations to support byte-aligned structures (because `void` would now be a byte-aligned structure). For non-byte-addressable systems, this increases memory usage (struct pointers get bigger) and code size (pointer dereferencing is now a lot more complicated). – Raymond Chen Nov 10 '18 at 15:18
  • @RaymondChen: I understand the problem, but I'm not sure that this would mandate `sizeof(struct Foo*) == sizeof(void *)`. The fact that `sizeof(struct Foo*) == sizeof(struct Bar*)`, while not explicitly mandated AFAIK, comes from the fact that any pointer to `struct` must be a complete type even with `Foo` and `Bar` being incomplete types (IOW, it must not depend on the actual definition of `Foo` and `Bar`); this would not apply to `void`, as it would be a builtin type - so, always already declared - as it is now. – Matteo Italia Nov 10 '18 at 16:05

2 Answers2

19

There is a proposal for this, it is p0146: Regular Void

Presented below is a struct definition that is analogous to what is proposed for void in this paper. The actual definition is not a class type, but this serves as a fairly accurate approximation of what is proposed and how developers can think about void. What should be noticed is that this can be thought of as adding functionality to the existing void type, much like adding a special member function to any other existing type that didn't have it before, such as adding a move constructor to a previously non-copyable type. This comparison is not entirely analogous because void is currently no ordinary type, but it is a reasonable, informal description, with details covered later.

struct void {
  void() = default;
  void(const void&) = default;
  void& operator =(const void&) = default;

  template <class T>
  explicit constexpr void(T&&) noexcept {}
};

constexpr bool operator ==(void, void) noexcept { return true; }
constexpr bool operator !=(void, void) noexcept { return false; }
constexpr bool operator <(void, void) noexcept { return false; }
constexpr bool operator <=(void, void) noexcept { return true; }
constexpr bool operator >=(void, void) noexcept { return true; }
constexpr bool operator >(void, void) noexcept { return false; }

It was received well in Oulu June 2016 meeting Trip Report:

Regular void, a proposal to remove most instances of special-case treatment of void in the language, making it behave like any other type. The general idea enjoyed an increased level of support since its initial presentation two meetings ago, but some details were still contentious, most notably the ability to delete pointers of type void*. The author was encouraged to come back with a revised proposal, and perhaps an implementation to help rule out unexpected complications.

I chatted with the author and he confirmed that it is basically waiting for an implementation, once there is an implementation he plans on bringing the proposal back.

There is extensive discussion in the paper about what changes and why, it is not really quotable as a whole but the FAQ questions addressed are:

  • Doesn't This Proposal Introduce More Special-Casing for void?
  • Why Isn't sizeof(void) Equal to 0?
  • Does This Break std::enable_if?
  • In Practice, Would This Break ABI Compatibility?
  • Doesn't constexpr_if Make Branching for void Easier?
  • Isn't It Illogical to Support some-operation for void?
  • Doesn't This Remove the Notion of "No Result?"
  • Isn't This a Change to the Meaning of void?
Shafik Yaghmour
  • 154,301
  • 39
  • 440
  • 740
  • 4
    It would be harder to differentiate between a pointer to something unknown (type-erasure the ultimate) and a `void`-object / array of `void`s. I wonder how implicit conversion of data-pointer to `void*` will fare. Sometimes, the lack of regularity is actually a safety-feature. – Deduplicator Nov 07 '18 at 21:07
  • Is `void` the base class of every other class? And of every scalar type? Bonus Q: Is `void` a virtual base (to avoid ambiguous conversions)? Trying to fit a complete consistent framework (or type system) over the inconsistency of a bunch of ad hoc rules is hard. – curiousguy Nov 10 '18 at 07:20
  • @curiousguy: read the proposal: it's essentially what I outlined in the question; `void` becomes a regular, instantiable type; current special rules about `void *` remain; base classes and such never come into play. – Matteo Italia Nov 10 '18 at 11:10
  • 1
    @MatteoItalia `void` cannot become 100% regular because there is still code using `(void)` for the empty argument list. – curiousguy Dec 14 '18 at 21:38
0
  • void is a type with an empty domain (it has not possible values);
  • struct foo { } is a type with a non-empty domain (there is one value of this type).

In other words, void is a bottom type while a potential struct void {} would be a unit type.

Replacing the first with second essentially breaks, well, the whole world. It's not entirely dissimilar from deciding that 0 equals 1 in C++.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • The analogy doesn't hold, it's more like replacing `bool` with `int` - you add possible values, but if you don't use them no harm is done. – Matteo Italia Dec 14 '18 at 20:36
  • 1
    Indeed you can't do `void x; return x;` or even `(void());`. But then `int f(void);` isn't the declaration of a function taking one "bottom" `void` value either. Nothing is regular in C/C++. That's why it's sooooo hard to do generics properly. – curiousguy Dec 14 '18 at 21:36
  • 2
    @curiousguy: C++ is indeed somewhat broken in that sense. And while that could be fixed, "properly" fixing things like this means discarding backwards compatibility in favor of that "smaller and clearer language struggling to get out" which Bjarne mentioned. That's my opinion anyway. – einpoklum Dec 14 '18 at 23:37
  • `void` is not a bottom type. `[[noreturn]] void` is a bottom ‘type’. – user3840170 Mar 28 '21 at 06:52
  • @user3840170: `[[noreturn]]` is an attribute of a function, not of a type. There is no such thing as a `[[noreturn]] void` type. – einpoklum Mar 28 '21 at 10:12
  • Thank you for explaining the scare quotes around ‘type’. – user3840170 Mar 28 '21 at 10:12