Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Differences between decltype(void()) and decltype(void{})

This is a follow-up of the question: What does the void() in decltype(void()) mean exactly?.


decltype(void()) compiles fine and what the void() means in this case is explained in the above mentioned question (actually in the answer).
On the other side, I noticed that decltype(void{}) doesn't compile.

What's the difference between them (in the context of a decltype at least)?
Why doesn't the second expression compile?


For completeness, it follows a minimal (not-)working example:

int main() {
    // this doesn't compile
    //decltype(void{}) *ptr = nullptr;
    // this compiles fine
    decltype(void()) *ptr = nullptr;
    (void)ptr;
}
like image 307
skypjack Avatar asked Sep 02 '16 21:09

skypjack


2 Answers

void() is interpreted as type-id when used with sizeof.
void() is interpreted as an expression when used with decltype.

I don't think void{} is valid in any context. It is neither a valid type-id nor a valid expression.

like image 162
R Sahu Avatar answered Nov 15 '22 23:11

R Sahu


(Building on discussion in the question comments)

Note: I was referencing C++17 or close-to for the answer. C++14 works the same way, the text difference is noted near the end of the answer.

void() is a special exception. See N4618 5.2.3 [expr.type.conv], emphasis mine:

1 A simple-type-specifier (7.1.7.2) or typename-specifier (14.6) followed by a parenthesized optional expression-list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer. If the type is a placeholder for a deduced class type, it is replaced by the return type of the function selected by overload resolution for class template deduction (13.3.1.8) for the remainder of this section.

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 (5.4). If the type is (possibly cv-qualified) void and the initializer is (), 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 (8.6) with the initializer. For an expression of the form T(), T shall not be an array type.

So void() is only valid because it's explicitly identified in [expr.type.conv]/2 as no initialization. void{} doesn't meet that exception, so it attempts to be a direct-initialized object.

tl;dr here through C++14 notes: You cannot direct-initialize a void object.

Rabbit-holing through N4618 8.6 [dcl.init] to see what's actually going on with void{}

  • 8.6/16 => direct-initialization
  • 8.6/17.1 => list-initialized, jump to 8.6.4
  • 8.6.4/3.10 => value-initialized
    • void() would have shortcut the above three with 8.6/11 => value-initialized, and then rejoined the trail, which is why the special exception in [expr.type.conv] is needed.
  • 8.6/8.4 => zero-initialized

Now, 8.6/6 defines zero-initialize for:

  • scalar
  • non-union class type
  • union type
  • array type
  • reference type

N4618 3.9 [basic.types]/9 defines scalar:

Arithmetic types (3.9.1), enumeration types, pointer types, pointer to member types(3.9.2), std::nullptr_t, and cv-qualified versions of these types (3.9.3) are collectively called scalar types.

N4618 3.9.1 [basic.fundamental]/8 defines arithmetic types:

Integral and floating types are collectively called arithmetic types.

So void is not an arithmetic type, so it's not a scalar, so it cannot be zero-initialized, so it cannot be value-initialized, so it cannot be direct-initialized, so the expression is not valid.

Apart from the initialisation, void() and void{} would work the same way, producing a prvalue expression of type void. Even though you cannot have a result object for an incomplete type, and void is always incomplete:

N4618 3.9.1 [basic.fundamental]/9 (bold mine):

A type cv void is an incomplete type that cannot be completed; such a type has an empty set of values.

decltype specifically allows incomplete types:

N4618 7.1.7.2 [decl.type.simple]/5 (bold mine):

If the operand of a decltype-specifier is a prvalue, the temporary materialization conversion is not applied (4.4) and no result object is provided for the prvalue. The type of the prvalue may be incomplete.


In C++14, N4296 5.2.3 [expr.type.conv] is differently worded. The braced form was almost an afterthought to the parenthesised version:

A simple-type-specifier (7.1.6.2) or typename-specifier (14.6) followed by a parenthesized expression-list constructs a value of the specified type given the expression list. If the expression list is a single expression, the type conversion expression is equivalent (in definedness, and if defined in meaning) to the corresponding cast expression (5.4). If the type specified is a class type, the class type shall be complete. If the expression list specifies more than a single value,the type shall be a class with a suitably declared constructor(8.5,12.1), and the expression T(x1, x2, ...) is equivalent in effect to the declaration T t(x1, x2, ...); for some invented temporary variable t, with the result being the value of t as a prvalue.

The expression T(), where T is a simple-type-specifier or typename-specifier for a non-array complete object type or the (possibly cv-qualified) void type, creates a prvalue of the specified type, whose value is that produced by value-initializing (8.5) an object of type T; no initialization is done for the void() case. [Note: if T is a non-class type that is cv-qualified, the cv-qualifiers are discarded when determining the type of the resulting prvalue (Clause 5). —end note]

Similarly, a simple-type-specifier or typename-specifier followed by a braced-init-list creates a temporary object of the specified type direct-list-initialized (8.5.4) with the specified braced-init-list, and its value is that temporary object as a prvalue.

The effect is the same for our purposes, the change relates to P0135R1 Wording for guaranteed copy elision through simplified value categories, which removed temporary object creation from expressions. Instead, the context of the expression provides a result object to be initialised by the expression, if the context needs it.

As noted above, decltype (unlike sizeof or typeid) doesn't provide a result-object for the expression, which is why void() works even though it cannot initialise a result object.


I feel that the exception in N4618 5.2.3 [expr.type.conv] ought to be applied to void{} too. It means that guidelines around {} get more complex. See for example ES.23: Prefer the {} initializer syntax in the C++ Core Guidelines, which would currently recommend decltype(void{}) over decltype(void()). decltype(T{}) is more likely to be where this one bites you...

like image 44
TBBle Avatar answered Nov 15 '22 23:11

TBBle