cplusplus / CWG

Core Working Group
23 stars 7 forks source link

CWG2760 [expr.const] immediate escalating expressions and default member initializers #354

Closed cor3ntin closed 9 months ago

cor3ntin commented 1 year ago

It is unclear whether default constructors of classes with immediate escalating default member initializers are immediate functions

consteval int f(int);
struct S {
  int x = f(0);
  S() = default;
};

int main() {
    S s;
}

In the above example

But is S::S() an immediate function?

It is unclear that p18.2 applies (and p15 is irrelevant because S is not an aggregate)

An immediate function is a function or constructor that is an immediate-escalating function F whose function body contains an immediate-escalating expression E such that E's innermost enclosing non-block scope is F's function parameter scope.

It would be inconsistent for it not to be, so this is either an oversight or me missing something again!

Proposed resolution

Modify [expr.const] /p18

An immediate function is a function or constructor that is

cor3ntin commented 1 year ago

Similarly

An aggregate initialization is an immediate invocation if it evaluates a default member initializer that has a subexpression that is an immediate-escalating expression.

An immediate-escalating expression shall appear only in an immediate-escalating function.

Is the intent that aggregate initialization constitute an immediate-escalating context?

cor3ntin commented 1 year ago

The more I try to think about it, the less it makes sense to me. Maybe we just want to say that

An aggregate initialization is an immediate-escalating expression if it evaluates a default member initializer that has a subexpression that is an immediate-escalating expression.

jensmaurer commented 1 year ago

CWG2760

cor3ntin commented 1 year ago

Thanks a lot, i think your complete proposed resolution makes sense to me.

cor3ntin commented 1 year ago

... or not!

Here is what gives me pause:

An aggregate initialization is an immediate invocation if it evaluates a default member initializer that has a subexpression that is an immediate-escalating expression.

An immediate invocation ([expr.const]) that is a potentially-evaluated subexpression ([intro.execution]) of a default member initializer is neither evaluated nor checked for whether it is a constant expression at the point where the subexpression appears.

An immediate invocation ([expr.const]) that is not evaluated where it appears ([dcl.fct.default], [class.mem.general]) is evaluated and checked for whether it is a constant expression at the point where the enclosing initializer is used in a function call, a constructor definition, or an aggregate initialization.

So, given

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

struct S {
    int i;
    int j = f(i);
};

S s(0);

To know whether the initialization of S is an immediate invocation, I need to try to try to evaluate f, which i can only do by...initializing S. There is a nasty circular dependency here and I don't how implementations may solve that except by trying to initialize twice, which, if possible, doesn't appear practical.

Note that it's very possible I'm missing something, but i think aggregates require a bit more thought or clarification. It's very different from, eg, a defaulted constructor where the default member initialization always happens in a non-immediate context that then escalates, before it is used. When initializing an aggregate we can't as easily decide the context we are currently in suddenly becomes immediate

One option would be to make the enclosing immediate-escalating function immediate, but that does not solve the above example.

To be clear, i think the resolution for this issue is fine but we probably want a separate issue to deal with aggregate initialization - it is a separate but related issue.