Non-literal types and constant expressions

Viewed 410
struct A {       
    ~A() {}
    consteval A() {}
    consteval auto f() {}
};

int main() {
    A{};
    //A{}.f(); //1
}

https://godbolt.org/z/4KPY5P7o7

This program is accepted by ICC, GCC and Clang, but rejected by MSVC which complains that the destructor is not constexpr in the immediate function invocation.

Adding the line marked //1 results in all four compilers rejecting the code.


Question: In either case, are the compilers correct, and if so why?


Note that the interesting part here is that A is non-literal due to the non-constexpr non-trivial destructor. Removing its declaration, all compilers accept both the variant with and without //1.

There are a few restrictions specific to non-literal types for constexpr/consteval functions and for constant expressions, but I don't think any of them should apply here. The restrictions are on return types, parameter types, types of local variable definitions, rvalue-to-lvalue conversions and modifications of objects. I think only the last one can apply here. But what exactly does modification in [expr.const]/5.16 mean and which object would be modified here?

I also think MSVC's complaint is incorrect since the destruction of the object shouldn't be part of its constructor's immediate invocation.

See also my earlier question inspiring this one: Consteval constructor and member function calls in constexpr functions

1 Answers

Updated with more exact references to the standard:

The pieces I found relevant (links are from the N4868 draft found here):

  • An immediate invocation is a full-expression [expr.const]
  • "Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created. ... The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression." [class.temporary]
  • "The argument list is the expression-list in the call augmented by the addition of the left operand of the . operator in the normalized member function call as the implied object argument ([over.match.funcs])." [over.call.func]
  • "A constant expression is either a glvalue core constant expression that ..., or a prvalue core constant expression whose ..." [expr.const]
  • "An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following: ... an invocation of a non-constexpr function;" [expr.const]
  • "An immediate invocation shall be a constant expression." [expr.const]
  • "An object or reference is usable in constant expressions if it is ... a temporary object of non-volatile const-qualified literal type whose lifetime is extended ([class.temporary]) to that of a variable that is usable in constant expressions," [expr.const]
  • "A type is a literal type if it is: ... a possibly cv-qualified class type that has all of the following properties: it has a constexpr destructor ([dcl.constexpr])," [basic.types]

Consider the following example:

struct A {       
  ~A() {} // not constexpr
  consteval int f() { return 1; }
};

template<class T>
consteval int f(T&& a) { return sizeof(a); }
consteval int f(int x) { return x; }
void g() {}

int main() {
  A a;
  f(a);           // ok
  a.f();          // ok
  f(a.f());       // ok
  f(sizeof(A{})); // ok

  f(A{});     // not ok TYPE 1 (msvc) or TYPE 2 (clang, gcc)
  A{}.f();    // not ok TYPE 1 (msvc) or TYPE 2 (clang, gcc)
  f((A{},2)); // not ok TYPE 1 (clang, msvc) or TYPE 2 (gcc)
  f((g(),2)); // not ok TYPE 1 (clang, gcc, icc, msvc)
}

Error diagnostics are about violating that immediate invocations should be constant expressions.

// msvc:
error C7595: 'f' ((or 'A::f')): call to immediate function is not a constant expression
// icc:
call to consteval function "f(T&&) [with T=A]" ((or "A::f" or "f(int)")) did not produce a valid constant expression
// clang:
error: call to consteval function 'f<A>' ((or 'A::f' or 'f')) is not a constant expression

Note that gcc does not mention the violation of this consteval/immediate function specific rule explicitly.

For the temporaries we receive two types of diagnostics from different compilers. Some see the problem in calling a non-constexpr destructor or function in a constant (full-)expression. TYPE 1:

// msvc:
note: failure was caused by call of undefined function or one not declared 'constexpr'
note: see usage of 'A::~A' ((or 'g'))
// icc:
note: cannot call non-constexpr function "g"
// gcc:
error: call to non-'constexpr' function 'void g()'
// clang:
note: non-constexpr function '~A' ((or 'g')) cannot be used in a constant expression

Others (except for icc, which is silent about it) highlight that non-literal type temporaries cannot be present in constant expressions. TYPE 2:

// gcc:
error: temporary of non-literal type 'A' in a constant expression
note: 'A' is not literal because:
note:   'A' does not have 'constexpr' destructor
// clang:
note: non-literal type 'A' cannot be used in a constant expression

I think for consteval consideration A{}.f() is equivalent to the f(A{}) case because of the implicit object parameter of A::f.

The surprising observation from Fedor that icc compiles A{A{}}.f() is true even if A::A(const A&) is implemented to call e.g. printf. The code compiles, but outputs nothing. I consider that a bug. Interestingly icc generates an error for the semantically very similar f(A{A{}}) variant.


My original post for reference (helps understanding some of the comments) :

For me the output diagnostics make sense. My mental model about immediate invocations is this: you are allowed to use an immediate function only within immediate contexts. An expression that contains anything else than constexpr operations is not an immediate context.

In your example the expression is not only an invocation of the constexpr constructor, but because the temporary is part of the expression, its destruction should also happen as part of the evaluation of the expression. Therefore your expression is no longer an immediate context.

I was playing around just calling the constructor with placement new to avoid the dtor call being part of the expression, but placement new itself is not considered constexpr either. Which is, I think, conceptually best explained by pointers should not present in immediate contexts at all.

If you remove ctor/dtor from the expression:

A a;
a.f();

then it compiles fine.

An interesting bug in ICC that it fails to compile A{}.f() even with a constexpr dtor, and you cannot convince it no matter how trivial definition your f has:

error: call to consteval function "A::f" did not produce a valid constant expression
      A{}.f();
          ^

while it compiles the simple a.f() variant listed above without any complaint.

Related