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.unary.op] Clarify through note whether indirection of a prvalue array is possible CWG2548 #6555

Open
Eisenwave opened this issue Sep 6, 2023 · 33 comments
Labels
cwg Issue must be reviewed by CWG.

Comments

@Eisenwave
Copy link
Contributor

Eisenwave commented Sep 6, 2023

See StackOverflow/Can you dereference a temporary array?.

There is some disagreement, and even after quite a bit of debate, people don't seem to find consensus on this one.

@languagelawyer's interpretation seems correct to me; which is that a in an expression *T{} where T = int[1], nothing necessitates an implicit conversion to a pointer, which would have made the indirection valid.

I suggest adding a note to [expr.unary.op] p1 along the lines of:

+[Note: Indirection through a prvalue of array type is impossible
+because array-to-pointer conversion does not take place. --end note]
@languagelawyer
Copy link
Contributor

languagelawyer commented Sep 6, 2023

I suspect CWG might want to treat this like CWG2548
(I (would) disagree with allowing either for array prvalues)

Also, http://eel.is/c++draft/conv.general#1.sentence-5 is really suspicious, I've asked @zygoloid why do we need http://eel.is/c++draft/basic.lval#6 when we have it UPD: seems not to be suspicious

@Eisenwave
Copy link
Contributor Author

Eisenwave commented Sep 6, 2023

(I (would) disagree with allowing either for array prvalues)

I assume by either you mean converting arrays for addition, and converting them for indirection. It would break too much code to disallow it for addition. I have seen, and have personally written lots of code in the style of:

algorithm(array, array + length);
// or in C++17
algorithm(array, array + std::size(array));

Of course, you could write this in terms of begin(array), end(array) if you were using C++11, but there's simply too much code that doesn't; either because of stylistic preferences or compatibility.
C developers tend to pass an array and a size usually, but I'm not sure whether that practice is universal.

Also, http://eel.is/c++draft/conv.general#1.sentence-5 is really suspicious, I've asked @zygoloid why do we need http://eel.is/c++draft/basic.lval#6 when we have it

I agree; [conv.general] p1 is really suspicious. Since the places where implicit conversions take place are explicitly specified, this really seems more like a note. The sentence is part of the definition of implicit conversions, so I suspect it's just supposed to hint at the fact that generally, implicit conversions are applied when necessary.

I think it would be useful to keep the last sentence as a note, which is likely CWG intention. I could be wrong though, and maybe this is very much intentional normative wording.

@languagelawyer
Copy link
Contributor

I have seen, and have personally written lots of code in the style of:

algorithm(array, array + length);
// or in C++17
algorithm(array, array + std::size(array));

Where array is a prvalue?!

@Eisenwave
Copy link
Contributor Author

Eisenwave commented Sep 6, 2023

Where array is a prvalue?!

Uh no, but CWG 2548 isn't restricted to prvalues, is it? Just

[...] is applied to an operand of array type.

If we're talking about prvalues only, then I agree; it wouldn't make sense to allow conversion there.

@languagelawyer
Copy link
Contributor

but CWG 2548 isn't restricted to prvalues, is it?

It is.

Just

[...] is applied to an operand of array type.

For array glvalues, https://eel.is/c++draft/expr.add#1.sentence-2 + https://eel.is/c++draft/basic.lval#6.sentence-1 already work

@RealLitb
Copy link

RealLitb commented Sep 6, 2023

In my opinion, this is even more suspicious considering the last note in expr.conv:

[Note 4: There are some contexts where certain conversions are suppressed.
For example, the lvalue-to-rvalue conversion is not done on the operand of the unary & operator.
Specific exceptions are given in the descriptions of those operators and contexts.
— end note]

Are they "suppressed" or are they simply not done? The note reads as though all implicit conversions would always apply if possible and would need to be suppressed in certain situations, but that doesn't appear to me to be the case. The &-operator does not do said conversion, because it doesn't expect/require a prvalue. I agree with Eisenwave on the above.

@languagelawyer
Copy link
Contributor

In my opinion, this is even more suspicious considering the last note in expr.conv

This looks like a C artifact. In C++, we seem to require conversions by http://eel.is/c++draft/basic.lval#6 and http://eel.is/c++draft/basic.lval#7

@languagelawyer
Copy link
Contributor

note in expr.conv

FYI, was not note in Jan 95, became in Apr 95

@Eisenwave
Copy link
Contributor Author

Eisenwave commented Sep 6, 2023

After further reading, I don't understand why CWG 2548 favors allowing conversion of prvalue arrays to pointers for the purpose of addition.

int* ptr = A{1, 2, 3} + 0;

would perform temporary materialization of the array so that you can do array-to-pointer conversion. However, I don't think there is any wording that would prevent the pointer from being instantly dangling. Lifetime extension is a separate effect that only works for references, to my knowledge.

It's unclear how you could benefit from this conversion other than to produce developer mistakes. You would still need to get the size of the array somehow, meaning you need a separate std::size(A{1, 2, 3}) or so. Maybe you could make this viable with a macro, but I doubt it would be of much use.

@languagelawyer
Copy link
Contributor

languagelawyer commented Sep 6, 2023

Ah. [conv.general]/1 says

A standard conversion sequence will be applied to an expression if necessary to convert it to an expression having a required destination type and value category.

According to [dcl.init.general]

The destination type is the type of the object or reference being initialized

So [conv.general]/1 seem to apply only to initialization and should be read together with the last bullet of how initialization works:

Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression.
A standard conversion sequence ([conv]) is used to convert the initializer expression to a prvalue of the cv-unqualified version of the destination type; no user-defined conversions are considered.
If the conversion cannot be done, the initialization is ill-formed.
When initializing a bit-field with a value that it cannot represent, the resulting value of the bit-field is implementation-defined.

@t3nsor
Copy link
Contributor

t3nsor commented Sep 6, 2023

I'm not sure whether that sentence in [conv.general]/1 is load-bearing. If I were sure it wasn't, then I would have proposed striking it for CWG1642, but I didn't have time to go through the whole standard to make sure.

@languagelawyer
Copy link
Contributor

After further reading, I don't understand why CWG 2548 favors allowing conversion of prvalue arrays to pointers for the purpose of addition.

I've asked to create the issue only because @zygoloid asked me and I wanted to have smth. to refer to as NAD, which is my suggested resolution (cplusplus/CWG#18)

Indeed, unlike for subscripting expression (https://timsong-cpp.github.io/cppwp/n4868/class.temporary#6.3), there is no temporary lifetime extension neither for + alone, nor for *(ARRAY_PRVALUE + J), so it is unclear why CWG wants to encourage developer mistakes.

@jensmaurer
Copy link
Member

One plausible use-case is something like this:

using A = int[3];
#define VALUE(i) (*(A{1,2,3} + i))
int x = VALUE(1);

Since we do have array temporaries these days, it seems plausible to allow them to appear where otherwise an array is valid.

@languagelawyer
Copy link
Contributor

One plausible use-case is something like this:

using A = int[3];
#define VALUE(i) (*(A{1,2,3} + i))
int x = VALUE(1);

Looks artificial to me, I think most would write

#define VALUE(i) A{1,2,3}[i]

@AlisdairM
Copy link
Contributor

AlisdairM commented Sep 6, 2023 via email

@jensmaurer
Copy link
Member

Looks artificial to me, I think most would write
#define VALUE(i) A{1,2,3}[i]

Why would that work? [expr.sub] p2 says

One of the expressions shall be a glvalue of type “array of T” or a prvalue of type “pointer to T” and the other shall be a prvalue of unscoped enumeration or integral type.

@languagelawyer
Copy link
Contributor

languagelawyer commented Sep 6, 2023

Why would that work? [expr.sub] p2 says

One of the expressions shall be a glvalue

7 Whenever a prvalue appears as an operand of an operator that expects a glvalue for that operand, the temporary materialization conversion is applied to convert the expression to an xvalue.

The same reason why Klass{}.member is ok, even though «For the first option (dot) the first expression shall be a glvalue.»

@jensmaurer
Copy link
Member

Ok. So, given that a[i] is equivalent to *(a+i), we now have a consistency argument why "a+i" should also work if "a" is a prvalue.

@languagelawyer
Copy link
Contributor

languagelawyer commented Sep 6, 2023

Shall temporary lifetime extension also work for consistency?
Union members creation? (https://timsong-cpp.github.io/cppwp/n4868/class.union#general-6.2)

@RealLitb
Copy link

RealLitb commented Sep 6, 2023

Ok. So, given that a[i] is equivalent to *(a+i), we now have a consistency argument why "a+i" should also work if "a" is a prvalue.

TIL that the two are not equivalent (different value category potentially). So I first "Like"ed the quoted comment, until I remembered what I learned today and removed the thumbs-up. Unfortunately, we left another elegant corner of C when breaking the equivalence.

@jensmaurer
Copy link
Member

I've amended CWG2548 with the additional concerns.

@Eisenwave
Copy link
Contributor Author

Example without macros:

template <int... N>
int foo() {
    using Array = int[sizeof...(N)];
    return Array{N...}[2];
}

int main() {
    return foo<0, 1, 2, 3, 4>();
}

@AlisdairM can you come up with an example that dereferences a temporary array or performs addition on it instead?

@Eisenwave
Copy link
Contributor Author

I suppose the note which I've suggested in this issue lives or dies with CWG 2548 then. If the change goes through, it's quite likely that dereferencing temporary arrays is also intentional.

Do I understand this correctly or are issues not as strongly related?

@jensmaurer
Copy link
Member

Thanks for the reminder. I've amended CWG2548 with the indirection case.

Whether we actually want to go there is probably something EWG should weigh in on, but consistency seems desirable.

@languagelawyer
Copy link
Contributor

Whether we actually want to go there is probably something EWG should weigh in on

Yes, please. Someone needs to present use-cases. Currently, it looks like the standard protects from writing meaningless code. There should be strong reasons to lift the restrictions.

@languagelawyer
Copy link
Contributor

Are they "suppressed" or are they simply not done?

More misuses of "suppressed": http://eel.is/c++draft/expr.call#1.sentence-4 http://eel.is/c++draft/dcl.init.ref#note-2

@Eisenwave
Copy link
Contributor Author

Related: cplusplus/papers#1633

@jensmaurer jensmaurer changed the title [expr.unary.op] Clarify through note whether indirection of a prvalue array is possible [expr.unary.op] Clarify through note whether indirection of a prvalue array is possible CWG2548 Sep 21, 2023
@jensmaurer jensmaurer added the cwg Issue must be reviewed by CWG. label Sep 21, 2023
@xmh0511
Copy link
Contributor

xmh0511 commented Sep 25, 2023

Ah. [conv.general]/1 says

A standard conversion sequence will be applied to an expression if necessary to convert it to an expression having a required destination type and value category.

According to [dcl.init.general]

The destination type is the type of the object or reference being initialized

So [conv.general]/1 seem to apply only to initialization and should be read together with the last bullet of how initialization works:

Otherwise, the initial value of the object being initialized is the (possibly converted) value of the initializer expression.
A standard conversion sequence ([conv]) is used to convert the initializer expression to a prvalue of the cv-unqualified version of the destination type; no user-defined conversions are considered.
If the conversion cannot be done, the initialization is ill-formed.
When initializing a bit-field with a value that it cannot represent, the resulting value of the bit-field is implementation-defined.

However, you oversight the [conv.general] p6

The effect of any implicit conversion is the same as performing the corresponding declaration and initialization and then using the temporary variable as the result of the conversion.

If the built-in implicit conversion only applies to initialization declaration, then almost [expr] that expects the converted operand won't work.

@languagelawyer
Copy link
Contributor

However, you oversight the [conv.general] p6

The effect of any implicit conversion is the same as performing the corresponding declaration and initialization and then using the temporary variable as the result of the conversion.

If the built-in implicit conversion only applies to initialization declaration, then almost [expr] that expects the converted operand won't work.

Could you elaborate more?

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 25, 2023

For example, [expr.ass] p3 says:

If the right operand is an expression, it is implicitly converted to the cv-unqualified type of the left operand.

In this context, there is no destination type, however, the built-in implicit conversion function does apply to this case. Furthermore, consider the Usual arithmetic conversions, which also do not specify "destination type". I think this issue arises from "shall". We should distinct "shall" and "requires" in these rules.

For example, when we say the operand shall be a certain type, that means, the original type must be that. Instead, when we say the operand is required to be a certain type, then it means any implicit conversion can be done if necessary.

@languagelawyer
Copy link
Contributor

languagelawyer commented Sep 25, 2023

In this context, there is no destination type

You quoted yourself that the effect is as having declaration+initialization of a variable.

Furthermore, consider the Usual arithmetic conversions, which also do not specify "destination type".

And why is this a problem? UPD: ah, you mean «destination type» is used in e.g. https://timsong-cpp.github.io/cppwp/n4868/conv.integral#3? Ok, this is kinda problematic

@xmh0511
Copy link
Contributor

xmh0511 commented Sep 26, 2023

You quoted yourself that the effect is as having declaration+initialization of a variable.

So, this effect still can work on the case *T{} where T is an array type, I think.

you mean «destination type» is used in e.g. https://timsong-cpp.github.io/cppwp/n4868/conv.integral#3?

Yes, I just think whether an implicit conversion can work depends on whether the context uses "destination type" is subtle. In the document, there are many places that uses the "destination type" with different meanings.

@languagelawyer
Copy link
Contributor

You quoted yourself that the effect is as having declaration+initialization of a variable.

So, this effect still can work on the case *T{} where T is an array type, I think.

[expr.unary.op]/1 does not say «implicitly converted»

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cwg Issue must be reviewed by CWG.
Projects
None yet
Development

No branches or pull requests

7 participants