cplusplus / CWG

Core Working Group
23 stars 7 forks source link

CWG2894 [expr.type.conv] `T{...}` Functional casts create prvalues of reference type `T` #536

Open Eisenwave opened 1 month ago

Eisenwave commented 1 month ago

Reference: [expr.type.conv] paragraph 2, sentence 3

Issue description

In T{...}, if T is a reference type, [expr.type.conv] paragraph 2, sentence 3 applies, stating:

Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer.

This is defective for references; there can be no prvalues of reference type.

Furthermore, it is not sufficiently clear that void(1, 2) and void{1} are not valid. All compilers reject these forms, and should.

Suggested resolution

Update [expr.type.conv] paragraph 2 as follows:

Let T be the specified type. The effect of the expression is as follows:
  • If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression.
  • Otherwise, if the type T is cv void and the initializer is the initializer shall be () or {} (after pack expansion, if any), and the expression is a prvalue of type void that performs no initialization.
  • Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized with the initializer has the same effect as direct-initializing an invented variable T t with the given initializer and then using t as the result of the expression. The result is an lvalue if the specified type T is an lvalue reference type or reference to function type, an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. If the initializer is a parenthesized optional expression-list, the specified type shall not be an array type.

To [expr.type.conv] paragraph 2, append an example:

void f() {
    unsigned(-1);     // OK, equivalent to (int) -1
    unsigned{-1};     // ill-formed, narrowing conversion

    void{};           // OK, prvalue of type void
    void(1);          // OK, equivalent to (void) 1
    void{0};          // ill-formed
    void(1, 2);       // ill-formed
    int(1, 2);        // ill-formed

    struct S { S(int, int); };
    S a = S(1, 2);    // OK, S(1, 2) is a prvalue
    S b = S(a);       // OK, equivalent to S b = (S) a;

    using R = S&;
    R r = R(a);       // OK, equivalent to R r = (R) a;
    R q = R{a};       // OK, same
}

[Editor's note: The example is intended to be educational and highlight the cases void(1, 2), R{a}, which currently have wording issues or related compiler bugs.]

Eisenwave commented 1 month ago

I'll revisit this and come up with some wording later.

Related to GCC Bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=115085.

Originally discovered at https://stackoverflow.com/q/78475217/5740428.

Supersedes https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1521

frederick-vs-ja commented 1 month ago

May be related: is it clear in [expr.type.conv] that void(1, 2) is ill-formed?

Eisenwave commented 1 month ago

May be related: is it clear in [expr.type.conv] that void(1, 2) is ill-formed?

I think it's semi-clear, but far from obvious. None of the other sentences really apply here, so the result is a prvalue of type void, direct-initialized using the initializer. [dcl.init] (to my knowledge) doesn't say anything about void explicitly, but you fall into the last "Otherwise" bullet (https://eel.is/c++draft/dcl.init#general-16.9), and the semantics here are impossible for void, so it's "ill-formed by nonsense".

Maybe I've missed some other wording that handles it more directly.

t3nsor commented 1 month ago

OK, here's a shot at a resolution. I copied some wording from [conv.general].

A simple-type-specifier or typename-specifier 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 for the remainder of this subclause. Otherwise, if the type contains a placeholder type, it is replaced by the type determined by placeholder type deduction ([dcl.type.auto.deduct]). Let T denote the resulting type.

If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression. Otherwise, if ~the type~ T is cv void ~and the initializer is () or {} (after pack expansion, if any)~, the initializer shall have at most one element and the expression is a prvalue of type void that performs no initialization. Otherwise, the expression ~is a prvalue of the specified type whose result object is direct-initialized with the initializer~ has the same effect as direct-initializing an invented variable t of type T from the initializer and then using t as the result of the expression. The result is an lvalue if T is an lvalue reference type or an rvalue reference to function type, an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. If the initializer is a parenthesized optional expression-list, ~the specified type~ T shall not be an array type.

Eisenwave commented 1 month ago

@t3nsor looks good to me normatively, but it's sufficiently complicated so that I'd split it into bullets.

Update: your wording seems to be missing a definition of T, which is mentioned in this defect, but not in the subclause. I've had to make some changes.

t3nsor commented 1 month ago

I've updated the proposed wording to include a definition of T.

Eisenwave commented 1 month ago

Actually now looking at this, the wording is even more defective because void{1} is not supposed to be valid. All compilers reject it, but I don't see how the old wording or the new wording would reject it.

Specifically, it would be wrong to say "the initializer shall have at most one element" instead of insisting on value-initialization because this makes void{1} valid. The first bullet handles void(1) and in every other case, you don't want any "input expressions" to be provided.

@t3nsor thanks for the good starting point; the post has its own proposed wording now.

Eisenwave commented 1 month ago

Also on another note, we should probably add wording to [dcl.init] that bans void x(1) and void z{1} etc. These are not intended to work, but I couldn't find wording that would actually forbid them.

frederick-vs-ja commented 1 month ago

Also on another note, we should probably add wording to [dcl.init] that bans void x(1) and void z{1} etc. These are not intended to work, but I couldn't find wording that would actually forbid them.

I think they've been banned by CWG2475 in [dcl.pre].

t3nsor commented 1 month ago

Ah I didn't realize that void{1} was supposed to be banned. Now that I look again at the old wording, it does seem clear that it's not allowed, since it falls through to the final case and is equivalent to void t{1}. I missed this case in my drafting.

jensmaurer commented 1 month ago

CWG2894

Eisenwave commented 2 weeks ago

Note to self: The current proposed resolution implies the creation of a separate temporary object in the case of non-reference T, but we don't want that.

The first half of the resolution is OK (up to void).

We can borrow wording from https://eel.is/c++draft/expr.static.cast#4 which has a mixed approach of invented variable for reference types, and forwarding the expression to direct-initialization otherwise.

t3nsor commented 2 weeks ago

I think bifurcating the wording that describes how the initialization is done for reference and non-reference types in all cases (including changing the existing [conv.general]/6) would be unfortunate. It obfuscates what we're really trying to say, which is "initialize the result of this expression the same way you would initialize this invented variable". I think we should just try to find a way of saying that that makes it clear that there's not really an intermediate variable.

t3nsor commented 2 weeks ago

Very rough idea:

~Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized ([dcl.init]) with the initializer.~ Otherwise, the expression is an lvalue if T is an lvalue reference type or an rvalue reference to function type, an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. The result of the expression ([basic.lval]) is determined by the rules for the direct-initialization of a variable of type T from the initializer.

Then repeat the same wording in [conv.general] and [expr.static.cast].

We could even factor out the part that tells you the value category, but I can't come up with a decent way of saying it.

Eisenwave commented 2 weeks ago

Very rough idea:

~Otherwise, the expression is a prvalue of the specified type whose result object is direct-initialized ([dcl.init]) with the initializer.~ Otherwise, the expression is an lvalue if T is an lvalue reference type or an rvalue reference to function type, an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. The result of the expression ([basic.lval]) is determined by the rules for the direct-initialization of a variable of type T from the initializer.

Then repeat the same wording in [conv.general] and [expr.static.cast].

"result is determined by the rules" sounds a bit too non-specific for my taste. We say what rules are involved in determining the result, but we don't say what the result is. Too much reading between the lines.

I really can't think of a good way around splitting the wording for references and object types. However, we can unify the wording between here and static_cast, e.g. by delegating the static_cast wording to [expr.type.conv].

We could even factor out the part that tells you the value category, but I can't come up with a decent way of saying it.

We can specify the type adjustment rules in [expr.type] p1 more concretely and then talk about a "result object or reference" in [expr.type.conv].

This should work and would be quite concise, but I personally dislike crutching on [expr.type].

jensmaurer commented 2 weeks ago

I've updated CWG2894 with the minimum fix. Any larger consolidations / rearrangements need specific wording suggestions.