Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Eisenwave opened this issue May 14, 2024 · 16 comments

Comments

@Eisenwave
Copy link

Eisenwave commented May 14, 2024

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
Copy link
Author

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
Copy link

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

@Eisenwave
Copy link
Author

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
Copy link

t3nsor commented May 14, 2024

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
Copy link
Author

Eisenwave commented May 14, 2024

@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
Copy link

t3nsor commented May 21, 2024

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

@Eisenwave
Copy link
Author

Eisenwave commented May 23, 2024

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
Copy link
Author

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
Copy link

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
Copy link

t3nsor commented May 23, 2024

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
Copy link
Member

CWG2894

@jensmaurer jensmaurer changed the title [expr.type.conv] T{...} Functional casts create prvalues of reference type T CWG2894 [expr.type.conv] T{...} Functional casts create prvalues of reference type T May 30, 2024
@Eisenwave
Copy link
Author

Eisenwave commented Jun 14, 2024

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
Copy link

t3nsor commented Jun 14, 2024

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
Copy link

t3nsor commented Jun 14, 2024

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
Copy link
Author

Eisenwave commented Jun 14, 2024

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
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants