dxxxxr0d1
Unambiguous prvalue void

Draft Proposal,

This version:
https://github.com/TBBle/WG21-void/blob/master/papers/dxxxxr0.bs
Author:
Paul "TBBle" Hampson <Paul.Hampson@Pobox.com>
Audience:
EWG
Project:
ISO JTC1/SC22/WG21: Programming Language C++

Abstract

void() is comparable to any type(), but void{} is needlessly invalid.

1. Proposal

An explicit prvalue of type void (spelled void() or "type_equivalent to void"()) has a very restricted range of use-cases, per [basic.fundamental]/9.

As with all T(), the 'Most Vexing Parse' results in void() being parsed as a parameterless function returning void, unless in a context where only an expression is possible.

The only use-case for an expression of type void which is not already in expression context is typeid. In this case, typeid(void()) is not equivalent to typeid(void). This is true of all other types, and the expression can be trivially written as typeid((void())).

sizeof(void()) will also see a function type, not an expression of type void, but since both are invalid operands for sizeof, the distinction for void is merely in the compiler diagnostic reported.

The usual method to disambiguate such parses is to write T{} instead. Since [p0135r1], this is explicitly disallowed by the standard: The literal spelling void() or the generic spelling T() where T somehow evaluates to void is explicitly called out in [expr.type.conv]/2 as "a prvalue of the specified type that performs no initialization". The equivalent initialiser spelling "{}" continues through the "otherwise" rules to direct-initialise a result object, failing when a requirement is hit to zero-initialise a prvalue of void, which is not possible, as zero-initialisation is only defined for scalars, class types, array types or reference types.

While that seems like a relatively minor use-case, it becomes more interesting when using a type expression then evaluates to void. Authors of generic code should prefer ([C++CG]) to use unambigous T{} to get a prvalue of type T, and should not be stymied when that might happen to be void.

So I propose that void{} (and generic spellings thereof) be treated identically to void() and generic spellings thereof, to close this gap.

The standard should also make use of void{} rather than void() in the few places it explicitly spells out the expression, leaving void() to be used only when a declaration of a parameterless function returning void is intended.

There is already a larger proposal in flight, [p0146r1], which would remove this special-case entirely. This current proposal would be obsolete if that proposal is accepted, and in the meantime represents a smaller, simpler change in the same direction.

3. Motivation

The frame of a motiviating case is:

#include <iostream>
#include <type_traits>
#include <cassert>

template <bool skip, typename Func, typename... Args,
  typename Return = std::result_of_t<Func(Args...)>>
Return maybe_skip(Func&& func, Args&&... args)
{
  if constexpr (skip) {           // <== #1
    return Return{};              // <== #2
  } else {
    return std::forward<Func&&>(func)(std::forward<Args&&>(args)...);
  }
}

int calculate(int a, int b, int c)
{
  return a + b * c;
}

void output(int value)
{
  std::cout << value << "\n";
}

int main()
{
  // Easy.
  int result = maybe_skip<false>(calculate, 5, 6, 7);
  assert( result == 47 );
  int noresult = maybe_skip<true>(calculate, 5, 6, 7);
  assert( noresult == 0 );

  // Fails at #2 without "consexpr" at #1
  maybe_skip<false>(output, result);

  // The motivating case, fails at #2 with
  // * "illegal initializer type 'void'" or
  // * "compound literal of non-object type 'void'" or
  // * "'initializing': cannot convert from 'initializer list' to 'void'"
  maybe_skip<true>(output, result);
}

Changing return Return{}; to return Return(); works today, but runs counter to advice such as "ES.23: Prefer the {} initializer syntax"

4. Wording

Relative to the latest C++ draft, [n4659].

Change in 8.2.3 Explicit type conversion (functional notation) [expr.type.conv] paragraph 2:

If the initializer is a parenthesized single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression. If the type is cv void and the initializer is () or {} , the expression is a prvalue of the specified type that performs no initialization. Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer. For an expression of the form T(), T shall not be an array type.

Change in the example at 10.1.7.4 The auto specificer [dcl.spec.auto] paragraph 9:

auto* g() { } // error, cannot deduce auto* from void(){}

Change in Table 14 of 17.5.3 Variadic templates [temp.variadic]:

Operator Value when parameter pack is empty
&& true
|| false
, void(){}

References

Normative References

[N4659]
Standard for Programming Language C++. 2017-03-21. Working Draft. URL: https://wg21.link/n4659

Informative References

[C++CG]
Bjarne Stroustrup; Herb Sutter. C++ Core Guidelines. URL: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
[P0135R1]
Richard Smith. Wording for guaranteed copy elision through simplified value categories. 20 June 2016. URL: https://wg21.link/p0135r1
[P0146R1]
Matt Calabrese. Regular Void. 11 February 2016. URL: https://wg21.link/p0146r1