struct A {
int i;
consteval A() { i = 2; };
consteval void f() { i = 3; }
};
constexpr bool g() {
A a;
a.f();
return true;
}
int main() {
static_assert(g());
}
https://godbolt.org/z/hafcab7Ga
The program is rejected by all of GCC, Clang, MSVC and ICC and replacing constexpr on g by consteval results in all four accepting it.
However, when removing the call a.f();, still with constexpr on g, only ICC still rejects the code. The other three now accept it.
I don't understand why this is the case. My understanding is that without consteval on g, the expression a.f() is not in an immediate function context, which will cause the member function call itself to be evaluated as separate constant expression, which then cannot modify the i member because the member's lifetime didn't begin during evaluation of that constant expression.
But why can the constructor perform the same operation on the same object, in the same context? Is a's lifetime considered to have begun during the evaluation of the consteval constructor?
Also note that the presence of the static_assert doesn't affect these results. Removing in addition constexpr completely from g then also doesn't change anything about the compiler behavior.
As noted by #Enlico, even replacing both A a; and a.f(); by A{}.f(); with constexpr on g results in all compilers except ICC accepting the code, although by my understanding, this expression should result in evaluation of two separate constant expressions for the immediate constructor invocation and for the immediate member function invocation. I think the latter call should behave exactly as a.f();, making this even more confusing.
(After reading #Barry's answer, I realize now that the last sentence didn't make any sense. Correction: A{} would be one constant expression for the constructor immediate invocation and A{}.f() as a whole would be the second constant expression for the member function immediate invocation. This is clearly different from the expression a.f().)
The rule is, from [expr.const]/13:
An expression or conversion is in an immediate function context if it is potentially evaluated and its innermost non-block scope is a function parameter scope of an immediate function. An expression or conversion is an immediate invocation if it is a potentially-evaluated explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.
Where, an immediate function is simply the term for (from [dcl.constexpr]/2):
A function or constructor declared with the consteval specifier is called an immediate function.
From the example:
struct A {
int i;
consteval A() { i = 2; };
consteval void f() { i = 3; }
};
constexpr bool g() {
A a;
a.f();
return true;
}
The call a.f() is an immediate invocation (we're calling an immediate function and we're not in an immediate function context, g is constexpr not consteval), so it must be a constant expression.
It, by itself, must be a constant expression. Not the whole invocation of g(), just a.f().
Is it? No. a.f() mutates a by writing into a.i, which violates [expr.const]/5.16. One of the restrictions on being a constant expression is that you cannot have:
a modification of an object ([expr.ass], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
Our object, a.i, didn't begin its lifetime within the evaluation of this expression. Hence, a.f() isn't a constant expression so all the compilers are correct to reject.
It was noted that A().f(); would be fine because now we hit the exception there - A() began its lifetime during the evaluation of this expression, so A().i did as well, hence assigning to it is fine.
You can think of this as meaning that A() is "known" to the constant evaluator, which means that doing A().i = 3; is totally fine. Meanwhile, a was unknown - so we can't do a.i = 3; because we don't know what a is.
If g() were a consteval function, the a.f() would no longer be an immediate invocation, and thus we would no longer require that it be a constant expression in of itself. The only requirement now is that g() is a constant expression.
And, when evaluating g() as a constant expression, the declaration of A a; is now within the evaluation of the expression, so a.f() does not prevent g() from being a constant expression.
The difference in rules arises because consteval functions need to be only invoked during compile time, and constexpr functions can still be invoked at runtime.
Related
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
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.
The following program
template<class T>
consteval auto foo(const T&) {
return 0;
}
template<class T>
consteval auto bar(const T& t) {
auto n = foo(t);
return n;
}
int main() {
static_assert(foo("abc") == 0);
static_assert(bar("abc") == 0);
}
is built fine in GCC, but Clang rejects it with the messages:
error: call to consteval function 'foo<char[4]>' is not a constant expression
note: in instantiation of function template specialization 'bar<char[4]>' requested here
static_assert(bar("abc") == 0);
note: function parameter 't' with unknown value cannot be used in a constant expression
auto n = foo(t);
Demo: https://gcc.godbolt.org/z/M6GPnYdqb
Is it some bug in Clang?
This is a clang bug. gcc and msvc are correct to accept it.
There are two relevant rules in question:
All immediate invocations must be constant expressions. This comes from [expr.const]/13:
An expression or conversion is in an immediate function context if it is potentially evaluated and either:
its innermost enclosing non-block scope is a function parameter scope of an immediate function, or
its enclosing statement is enclosed ([stmt.pre]) by the compound-statement of a consteval if statement ([stmt.if]).
An expression or conversion is an immediate invocation if it is a potentially-evaluated explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.
And touching an unknown reference is not allowed in constant expressions (this is [expr.const]/5.13):
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 id-expression that refers to a variable or data member of reference type unless the reference has a preceding initialization and either
it is usable in constant expressions or
its lifetime began within the evaluation of E;
For more on this latter rule, see my post on the the constexpr array size problem and my proposal to resolve this (hopefully for C++23).
Okay, back to the problem. foo is obviously fine, it doesn't do anything.
In bar, we call foo(t). This is not a constant expression (because t is an unknown reference), but we are in an immediate function context (because bar is consteval), so it doesn't matter that foo(t) is not a constant expression. All that matters is that bar("abc") is a constant expression (since that is an immediate invocation), and there's no rule we're violating there. It is pretty subtle, but the reference t here does have its lifetime begin within the evaluation of E -- since E here is the call bar("abc"), not the call foo(t).
If you mark bar constexpr instead of consteval, then the foo(t) call inside of it becomes an immediate invocation, and now the fact that it is not a constant expression is relevant. All three compilers correctly reject in this case.
C++20 will have the new consteval keyword and constexpr destructor if all goes well.
Sadly no compiler that I know of implements consteval at this time.
Will the following code be valid?
struct A {
constexpr ~A() {}
};
consteval A f() {
return A{};
}
void test() {
A a;
a = f(); // <-- here
}
The issue comes from the line I marked — the destructor of the temporary A returned by f need to be called. but it should be called at the end of the full expression. so outside of the immediate evaluation.
I didn't find any quotes from the consteval and constexpr destructor papers that explicitly forbid this, but I can't see how it could be correct.
Will the following code be valid in C++20?
What should happen with this code?
Note:
In the consteval paper, this example is given. Here the consteval function is called outside of constant context.
consteval int sqr(int n) {
return n*n;
}
constexpr int r = sqr(100); // Okay.
int x = 100;
int r2 = sqr(x); // Error: Call does not produce a constant.
I think this code is fine.
The salient aspect of consteval is [expr.const]/12:
An expression or conversion is in an immediate function context if it is potentially evaluated and its innermost non-block scope is a function parameter scope of an immediate function. An expression or conversion is an immediate invocation if it is an explicit or implicit invocation of an immediate function and is not in an immediate function context. An immediate invocation shall be a constant expression.
In
void test() {
A a;
a = f(); // <-- here
}
f() is an immediate invocation (it is an explicit invocation of an immediate function that is not in an immediate function context). So the requirement is that f() has to be a constant expression.
Note: just f(), not a = f();, there is no requirement that the assignment operator here is a constant expression.
Everything that f() invokes is perfectly fine. All of A's special member functions are invocable during constant evaluation time. The result of f() is a permitted result of a constant expression ([expr.const]/10) likewise because it does not trigger any of the restrictions there either (A has no pointer or reference members, so trivially none of them refer to objects without static storage duration).
Ultimately, the question of constant evaluation boils down to going through all the list of restrictions and seeing if anything breaks. I don't think we violate any of the rules, so this should be fine.
I wonder how a compiler treats lambda functions as opposed to regular functions. Even excluding the capture-list, as I think it's called, it seems to behave slightly differently.
For example, as used in my last post, Trying to pass a constexpr lambda and use it to explicitly specify returning type, I used a constexpr lambda and passed it as a regular function parameter. I quote a part of the answer.
Parameters to constexpr functions are not themselves constexpr objects - so you cannot use them in constant expressions.
template <typename Lambda_T>
constexpr static auto foo(Lambda_T l) {
return std::array<event, (l())>{};
}
// Compiles with GCC (C++17), though ill-formed (according to the answer of my last post)
Though, what catches my eye is that this does compile passing a constexpr lambda, but does not compile passing a constexpr regular (global) function. What causes this difference?
And are there other differences of behaviour of the compiler between regular functions and lambda-functions?
Edit: Example of Lambda-implementation
foo([](){ return 4; }); // C++17 Lambda's are implicitly constexpr
The lambda is basically a wrapper for the value in this case.
Edit: Global function
Whenever a global function is passed, the compiler will complain - as opposed to using a lambda - that this function, regardless of whether it is defined constexpr or not, cannot be used in a constant condition:
prog.cc:8:20: error: 'l' is not a constant expression
Removing some extraneous stuff from your snippet, we get
template<typename T>
constexpr void foo(T t)
{
constexpr int i = t();
}
constexpr int f() { return 42; }
auto l = []{ return 42; }
Remarks:
You are attempting to use t() as a constexpr within foo. foo could in fact be a normal function and still behave the same.
A lambda isn't a function. It is an anonymous struct with an operator().
struct L { constexpr int operator()() const { return 42; } };
T is deduced as a type equivalent to L in the call foo(l).
T is deduced as int(*)() in the call foo(f).
In both cases, t isn't a constexpr within foo.
From [expr.const]
An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions: [...]
an lvalue-to-rvalue conversion unless [...]
a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of such an object, or
a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;
Calling through a int(*)() requires a lvalue-to-rvalue conversion. t isn't an object defined with constexpr, neither did it begin its lifetime within the evaluation of t(). Therefore t() isn't a constexpr in foo(f).
Calling operator() doesn't require a lvalue-to-rvalue conversion. Since there is no member access, it is simply a constexpr function call. Therefore t() is a constexpr in foo(l).
There is one more step from core constant expressions to constant expressions, but isn't important for the discussion here.
For example the code below doesn't compile unless incr() is declared constexpr:
int incr(int& n) {
return ++n;
}
constexpr int foo() {
int n = 0;
incr(n);
return n;
}
Looking at §7.1.5/3 in C++14 we have:
The definition of a constexpr function shall satisfy the following
constraints:
(3.1) — it shall not be virtual (10.3);
(3.2) — its return type shall be a literal type;
(3.3) — each of its parameter types shall be a literal type;
(3.4) — its function-body shall be = delete, = default, or a compound-statement that does not contain
(3.4.1) — an asm-definition,
(3.4.2) — a goto statement,
(3.4.3) — a try-block, or
(3.4.4) — a definition of a variable of
non-literal type or of static or thread storage duration or for which
no initialization is performed.
Two paragraphs later, in [dcl.constexpr]/5:
For a non-template, non-defaulted constexpr function or a non-template, non-defaulted, non-inheriting
constexpr constructor, if no argument values exist such that an invocation of the function or constructor
could be an evaluated subexpression of a core constant expression (5.20), or, for a constructor, a constant
initializer for some object (3.6.2), the program is ill-formed; no diagnostic required.
No argument exists such that foo() could be a core constant expression because of incr(), therefore the program is ill-formed (NDR).
What you're looking for is § 5.19:
A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:
This applies to the evaluation of an expression that is a constexpr function call. That is, calling a constexpr function will be a 'core constant expression' if evaluating the function, i.e. executing the body of the function according to the rules of the C++ abstract machine, doesn't do any of the things forbidden in the list given in § 5.19.
One of items in the list is:
an invocation of a function other than [...] a constexpr function
So to apply this to your example: evaluating the expression foo() evaluates a call to a function incr() which is not a constexpr function, which means that the expression foo() is not a core constant expression.
Further, since the above is true for all possible invocations of your function foo, the rule in § 7.1.5/5 kicks in and means that your example program is ill-formed, no diagnostic required, even if you never actually call foo().
As Ben Voigt points out a constexpr function can contain calls to non-consexpr functions, so long as the particular evaluation of the function does not actually evaluate any such function call (or it appears in a context that does not require a constant expression).
The restrictions in 5.19 only pertain to what expressions actually end up being evaluated as part of the evaluation of an expression.
For example:
#include <iostream>
int incr(int &n) { return ++n; }
enum E {be_constexpr, not_constexpr};
constexpr int foo(E e = be_constexpr) {
int n = 0;
if (e == not_constexpr) { incr(n); }
return n;
}
int main() {
constexpr int a = foo(); // foo() is a constant expression
int b = foo(not_constexpr); // may or may not evaluate `foo(non_constexpr)` at runtime. In practice modern C++ compilers will do compile-time evaluation here even though they aren't required to.
// constexpr int c = foo(not_constexpr); // Compile error because foo(not_constexpr) is not formally a constant expression, even though modern compilers can evaluate it at compile-time.
std::cout << a << ' ' << b << '\n';
}
It doesn't.
The following is allowed, even though it does exactly what you surmise is forbidden:
int incr(int& n) {
return ++n;
}
constexpr int foo(bool x) {
int n = 0;
if (x) incr(n);
return n;
}
The code in your question is disallowed by the rule with Barry quoted in his answer. But in my variation, there does exist a set of parameters (specifically, false) with which invocation of foo results in a compile-time constant expression.
Note that a diagnostic isn't required -- a conforming compiler could allow your version to compile as well.