cplusplus / CWG

Core Working Group
23 stars 7 forks source link

[class.temporary] Optional temporary objects during parameter/return value passing in constant expression evaluation #585

Open keinflue opened 1 month ago

keinflue commented 1 month ago

Full name of submitter (unless configured in github; will be published with the issue): Benjamin Sch.

Reference (section label): [class.temporary]/3, [expr.const]/1

Issue description:

Resolution of CWG 2022, modified by CWG 2278, makes sure that whether or not an expression is a constant expression doesn't depend on unspecified compiler choices for copy elision.

The resolutions don't cover the following modification of the example in CWG 2022:

struct A {
    void *p;
    constexpr A() : p(this) {}
};

constexpr A f() { return A(); }

constexpr A b = f();

Here implementations are still permitted to introduce a temporary object for f's result object per [class.temporary]/3, causing initialization of b to possibly be ill-formed.

Suggested resolution:

Disallow introduction of a temporary during constant expression evaluation with the same wording as used for resolution of the issues mentioned above.

Modify [class.temporary]/3:

[...] implementations are permitted to create a temporary object to hold the function parameter or result object, except where the expression is evaluated in a context requiring a constant expression ([expr.const]) and in constant initialization ([basic.start.static]). [...]

Modify [expr.const]/1:

[...] Expressions that satisfy these requirements, assuming that copy elision is not performed and that temporary objects to hold function parameter or result objects are not created ([class.temporary]), are called _constant expressions_.

keinflue commented 1 month ago

In the case of result objects, the most recent possible resolution in CWG 2868 would go the other direction and make my example guaranteed ill-formed. Even with that resolution, the issue remains for parameter passing.

jensmaurer commented 1 month ago

The intent of CWG2868 is to make b.p an invalid pointer value (because the storage of the original "A" object has ended). We generally want constant evaluation to behave like runtime evaluation as much as possible, so your example is rightfully ill-formed (bad result of constant evaluation) under CWG2868.

Could you formulate an example for parameter passing, please?

keinflue commented 1 month ago

I suppose the situation is less severe in the case of parameter passing. The best I can come with at the moment would be

#include<iostream>

struct A {
    A* p;
    constexpr A() : p(this) {}
};

constexpr bool f(A a) { return &a == a.p; }

int main() {
    constexpr bool b1 = f({});
    bool b2 = f({});
    std::cout << b1 << b2 << "\n";
}

All four outputs 00, 01, 10 and 11 are currently permitted. GCC outputs 00, i.e. it introduces a temporary at compile time and run time. Clang and MSVC output 10, i.e. they introduce the temporary only at run time. (https://godbolt.org/z/cMTTEzcbb)

frederick-vs-ja commented 1 month ago

We generally want constant evaluation to behave like runtime evaluation

IMO we cannot eliminate all inconsistencies here, because of we want to allow some types meeting some conditions to be passed and returned in registers, while the concret conditions depend on platforms.

If we want to go for the current approach for CWG2868, perhaps we should say "If the function call is performed in the evaluation of a constant expression ([expr.const]), such a temporary object is always created in argument passing." in [class.temporary] p3.


May be off-topic. Let me check what's happening before & after CWG2868...

  1. Since C++17 (P0135R1), RVO ((Unnamed) Return Value Optimization) is generally guaranteed. However, in order to return in registers, when a prvalue (of a suitable trivially returnable type) is evaluated in a return statement with the "objectness" detected, we have to make the prvalue to be materialized to a temporary object. And then the result object of the function call (which can be temporary or non-temporary) is considered trivially copied/moved from that temporary object.
  2. When the return type is too large to be returned in registers, a return slot must be used. In this case, an implementation is free to perform RVO or not. Semantically, if RVO is considered performed, it will be possible to have the address of the result object recorded before returning and sometimes the optimizer have to assume this happens. On the other hand, if RVO is not performed, the optimizer can assume that the address of the result object can only be obtained after returning, but the implementation might need to copy the large object (although the copy is bitwise).
  3. The intent of CWG2868 seems to be allowing that when trivially returning a prvalue, i. the materialization is directly performed in the return slot, and ii. the implementation assumes the address of the return slot obtained before returning to be invalid, i.e., the result object is not aliased.
  4. The current resolution of CWG2868 (2024-05-03) seems more aggressive. It also removes the freedom of choice for performing RVO or not in constant evaluation - which is consistent with the intent expressed in CWG2022.
  5. The currently wording change for reinterpret_cast in P2434R1 possibly defeats the intent of CWG2868. Since i. when materialization is directly performed in the return slot, the address of the return slot can be recorded as an integer by reinterpret_cast, and then, ii. after returning, on common platforms, the integer can be reinterpret_cast'd back to a pointer which properly points to the result object due to the angelic nondeterminism.
keinflue commented 1 month ago

I don't know whether this is a concern with current implementations, but if the temporary object is mandatory it may require keeping around double the stack size during constant evaluation. Both the temporary object and the parameter object can be accessed through the pointers a.p and &a, respectively, and are alive during the function call. The parameter object at least may also be modified and if that happens, the old state of the temporary object, which may be large, must be kept around.

This should not be a concern at run time because the temporary is not mandatory and doesn't need to be used for large objects.

Btw., [class.temporary]/3 doesn't specify the type of the temporary object. Is it X or const X, i.e. can the temporary object be modified?

jensmaurer commented 1 month ago

If we want to do something about parameter objects, we'll likely do the two-step temporaries as for the return value, so that internal pointers become invalid, but memory can otherwise be reused.