Non-literal types and constant expressions
Asked Answered
M

1

10
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

Maice answered 12/1, 2022 at 17:39 Comment(7)
What exactly are you trying to do? The only reason to make all of the constructors for a type consteval is if you only intend for that object to ever be seen within constant expressions. But you seem to want to keep using them like they're regular objects that you can create anywhere.Breakwater
@NicolBolas If I am not mistaken std::format uses a type with only a consteval constructor outside constant expressions to implement the compile-time format string check. I am wondering what exactly the limits for this are. But the question here is more specifically about the interpretation of the cited paragraph of the standard. I could have probably constructed a similar example with only constexpr, e.g. godbolt.org/z/YoT93nvod. I will make it clearer in the question.Maice
@NicolBolas Sorry, correction: Without consteval I wasn't able to construct an example in which I can call f on the A object without the destructor call being part of the constant expression. The restriction on literal types for constexpr variables seems to make that impossible. So I couldn't get to form an example in which the cited paragraph matters.Maice
If one declares the destructor ~A() as constexpr then all error are gone: gcc.godbolt.org/z/z8W51efYoFernanda
@Fernanda Yes, that is clear because then A is a literal type. But non-literal types have restrictions in constant expression contexts, which is what I want to understand better. I don't know of a use case where this is required (and there probably is none), but I think that the compilers are wrong to reject the code. I'd just like to know if I am understanding the standard correctly.Maice
One more observation. Despite A{}.f(); is rejected by all; A{A{}}.f(); is accepted by ICC. Demo: godbolt.org/z/GfYreYK9MFernanda
See #65396585Pantry
P
3

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.

Pylle answered 27/1, 2022 at 11:11 Comment(9)
Please note that I intentionally tagged the question with language-lawyer, so some quotes or references to the standard would be appreciated. For example the term "immediate context" is used in the standard in relation to template instantiations (github.com/cplusplus/draft/search?q=%22immediate+context%22). There is "immediate function context" (eel.is/c++draft/expr#const-13), but in my example nothing inside main is in an immediate function context, since main is not an immediate function (assuming my understanding of the standard is correct).Maice
I do not believe that your first paragraph is correct. If that were true you couldn't e.g. write x = f(); where f is a consteval function and the assignment operator of x is not constexpr. That does not seem intended. It should be fine for an immediate invocation to be just a subexpression, independent of whether the larger expression is a constant expression.Maice
For example like this, which is accepted by all compilers: godbolt.org/z/TdWhv79v9Maice
So if I understand you correctly, you are saying that in the original program in the question, MSVC is correct to reject, because A{} is an immediate invocation and therefore also a full expression which must be a constant expression. Because temporaries are destroyed at the end of the full-expression, the destructor call should be part of this full expression. But with that reasoning, wouldn't that make using temporaries created by consteval constructor call completely unusable outside other consteval functions.Maice
For example this program should then also be ill-formed, since the temporary is destroyed before f's body is executed?Maice
Yes, my interpretation is that your linked program becomes ill-formed in case your dtor is not constexpr. With constexpr dtors you are ok to use temporaries in consteval invocations as well.Pylle
Also note that the temporary should only be destroyed after executing f's body.Pylle
I am talking about the code in the link as is, with constexpr destructor. So you are saying that in A{}; the destructor call is part of the immediate invocation of the constructor and in f(A{}); (where f is not consteval) it is not, because of lifetime extension I presume? The destructor call should then not be required to be constexpr. Then if f is consteval you are saying that the point of destruction becomes part of the immediate invocation of f?Maice
Yes, that was my reasoning. I quoted the part "An object or reference is usable in constant expressions", which would imply that a constexpr dtor is a must in a constant expression. Reading [const.expr] more I found "usable in constant expressions" is a more of a technical term. See "An object or reference is usable in constant expressions ... the initializer of a variable that is usable in constant expressions or has constant initialization ([basic.start.static])". In this new interpretation A{} is not a function call, but it is constant initialization.Pylle

© 2022 - 2024 — McMap. All rights reserved.