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

[expr.call] The result type of a function call and the value category relying on it #5415

Closed
xmh0511 opened this issue Apr 26, 2022 · 10 comments

Comments

@xmh0511
Copy link
Contributor

xmh0511 commented Apr 26, 2022

[expr.call] p14 says

A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.

So, the type of the result of a function call is significant to determine the value category of the function call. However, it's unclear the type of the result in a normal function call. [expr.call] p9 just says

The result of a function call is the result of the possibly-converted operand of the return statement ([stmt.return]) that transferred control out of the called function (if any), except in a virtual function call if the return type of the final overrider is different from the return type of the statically chosen function, the value returned from the final overrider is converted to the return type of the statically chosen function.

It does specify the destination type for the case that the return types of the final overrider and the statically chosen function are different. However, for the conversion in the first step, it is vague to the destination type of the conversion(if any).

The result of a function call is the result of the possibly-converted operand of the return statement ([stmt.return]) that transferred control out of the called function (if any)

When we build up a conversion, we usually use the utterance: convert the expression E to type T. For the above case, we lack to specify the destination type T(in the first conversion). The improvement might be:

The result of a function call is:

  • if no conversion is necessary, the operand of the return statement.
  • Otherwise, the result of the conversion that implicitly converts the operand of the return statement to the return type of the called function.

where the return statement transferred control out of the called function (if any), except in a virtual function call if the return type of the final overrider is different from the return type of the statically chosen function, the result of the final overrider is converted to the return type of the statically chosen function.

Moreover, [conv.general] p6 defines what the result of a conversion is.

using the temporary variable as the result of the conversion.

The modified rule together with [conv.general] p6 are clear to clarify the value category that relies on the result type of the function call.

struct A{
   A(int);
};
A fun(){
  return 0;
}

The result of the conversion is defined as

A t = 0;

The result of the call (t) is a prvalue of class A, thus the function call is a prvalue.

@frederick-vs-ja
Copy link
Contributor

It seems a bit clearer to use return type instead of result type after excluding (pseudo-)destructor calls (although they are not always the same, the difference doesn't matter here).

(Pseudo-)destructor calls are already specified to have type void and thus can't be glvalues.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 26, 2022

It seems a bit clearer to use return type instead of result type

Yes, It was my original thought before proposing the suggestion. However, it still does not clarify the result of the function call, as pointed out in this issue, we lack to specify the destination type of the conversion. So, instead of replacing result type with return type for [expr.call] p14, It might be better to directly clarify the result of the function call.


You reminded me that we should have a special rule to specify the result/value category of the function call whose return type is void, since either it has no operand of the return statement or it is a pseudo-destructor.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 26, 2022

Change [expr.call] p9 to

A function call where the return type of the function is T has the same value category and result as:

  • if T is cv void, a prvalue of T that has no result.
  • Otherwise, if the operand of the return statement ([stmt.return]) that transferred control out of the called function (if any) is E, for an invented declaration T t = E;
  • If no conversion is required and E is an expression, E.
  • Otherwise, the unqualified-id t; t is an lvalue if T is an lvalue reference type or an rvalue reference to function type ([dcl.ref]), an xvalue if T is an rvalue reference to object type, and a prvalue otherwise.
    [note: E may be a braced-init-list. --end note]

@jensmaurer
Copy link
Member

I don't think this is correct. For example,

int f()
{
  int x;
  return x;
}

I think this qualifies as "no conversion is required". The operand of the return statement is an lvalue, yet the function call f() obviously yields a prvalue.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 27, 2022

Why wouldn't an lvalue-to-rvalue conversion apply to the operand x in that example? As said in [basic.lval] p5

The result of a prvalue is the value that the expression stores into its context; a prvalue that has type cv void has no result.

Since the function call f() is a prvalue, its result should be the value stored in x where the value is fetched by the way specified in [conv.lval] p3.4:

Otherwise, the object indicated by the glvalue is read ([defns.access]), and the value contained in the object is the prvalue result.

I think the proposed suggestion is not good for class type, consider this example

struct B{};
B fun1(B& b){
    return b;
}

We have said that converting an argument of a class type to the same type is an identity conversion, which means no conversion is required. So, I think the proposed suggestion might be simplified to

Otherwise, if the operand of the return statement ([stmt.return]) that transferred control out of the called function (if any) is E, S is (E) if E is an expression that is not an assignment-expression, or E otherwise. For an invented declaration T t = S;, the function call has the same result and value category as the unqualified-id t; t is an lvalue if T is an lvalue reference type or an rvalue reference to function type ([dcl.ref]), an xvalue if T is an rvalue reference to object type, and a prvalue otherwise. The program is ill-formed If the declaration would be ill-formed.

Without considering what the original operand is. I think the declaration modeling a copy-initialization is consistent with [dcl.init.general] p14.


With this suggestion, I think we should remove the last sentence in [stmt.return] p2, or change it to a note

the return statement initializes the returned reference or prvalue result object of the (explicit or implicit) function call by copy-initialization from the operand.

Because, when a function call is a prvalue, it is not limited to being used to initialize an object, such as to be an operand of the operations(i.e. computes the value), that is, there is no result object to be initialized. And what is said in this sentence is covered by the suggestions. Also, it could clarify #4107

@jensmaurer
Copy link
Member

See also #4847 and #4723.

The wording should be clear that the type and value category of a function call only depend on the return type written in the function declaration. I think it would be helpful to separate that from the determination of the result (i.e. the value that is returned). I'm wondering whether we could simply say that the operand of the evaluated return copy-initializes the result, which works for both lvalues and prvalues, I think, and does the conversions we want.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 27, 2022

I'm wondering whether we could simply say that the operand of the evaluated return copy-initializes the result

As I said above, the function call is not necessarily used to initialize something. Consider this snippet

fun() + 1;

There is no stuff to be initialized(especially, no result object), we just need to read the value for computation. So, the current rule for the return statement obviously cannot cover this usage.

Indeed that using "return type" to directly determine the value category of a function call is clearer. Nonetheless, The result of a function call is also needed to be clarified.


I think it would be helpful to separate that from the determination of the result

Yes, I also think so. However, IMO, to determine the result of a call, we should first determine what the value category is(e.g. a glvalue determine the object or function, a prvalue determines the value), since this is a chain then why wouldn't integrate them into a rule.


For the return statement, I just think the evaluated return statement indicates/provides the corresponding initializer for the invented declaration, use the invented variable as the result of the function call is clear wherever we use the function call as an expression in any context(initialization or computation), that is, we don't need to recall the return statement when considering what the result is, reduce the baggage, I think.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 27, 2022

The example in #4847 actually wants to say we should consider any conversions required to match the operand of the return statement to the return type as part of the initialization of the invented declaration, in order to avoid the ambiguity when we say certain conversions are not considered when initializing the actual result object.

With the help of the suggestion, it is easy to interpret the example in #4847 and the corresponding referenced rule will be well.

struct B{
  B(int){}
};
struct A{
  operator B(){   
     return 0;
  }
};
A a;
B const& rf = a;  //#1

the function call to A::operator B is a prvalue defined as

 B t = 0;

t is the result of A::operator B(); and it is a prvalue, which means the result object will be initialized by the constructor B::B(int) with the argument 0 wherever the result is used to initialize an object. According to [dcl.init.ref] p5.4.1, the effect at #1 is the same like:

B const& rf(t);

The direct-initialization obeys [dcl.init.ref] p5.3, no user-defined conversion is required in this initialization.

@xmh0511
Copy link
Contributor Author

xmh0511 commented Apr 27, 2022

The issue in #4723 might be fixed by:

The copy-initialization of the returned reference or prvalue result object of the call is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement...

From a certain perspective, I think the returned reference is the invented variable that is of reference type.

result object case: https://godbolt.org/z/orTWM5Gf7

returned reference case: https://godbolt.org/z/3zq3f5hcd

@xmh0511
Copy link
Contributor Author

xmh0511 commented Nov 30, 2022

What the result of a function call is may be clarified by cplusplus/CWG#186, and therefore what the type of the result is. Or, we may just need to use the return type in the rule.

@xmh0511 xmh0511 closed this as completed Nov 30, 2022
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

3 participants