7

I wrote the following code that has two lambdas. One of them explicitly captures i while the other doesn't. Note i is constexpr so we don't need to capture it explicitly.

My question is why func(lambda2) doesn't compile while func(lambda) does? Note lambda doesn't explicitly capture i while lambda2 has i in its capture list. Demo

template <typename T>
constexpr auto func(T c) {
    constexpr auto k = c; 
    return k;
};


int main() {
    
    constexpr int i = 0;
    
    constexpr auto lambda = []()constexpr { constexpr int j = i; return j; };  //compiles
    constexpr auto lambda2 = [i]()constexpr { constexpr int j = i; return j; };//compiles
   
    constexpr auto a = func(lambda); //compiles as expected

    
    constexpr auto b = func(lambda2); //this doesn't compile why?
    
} 

Clang says:

error: constexpr variable 'k' must be initialized by a constant expression
    3 |     constexpr auto k = c; 
      |                    ^   ~
/home/insights/insights.cpp:18:24: note: in instantiation of function template specialization 'func<(lambda at /home/insights/insights.cpp:13:30)>' requested here
   18 |     constexpr auto b = func(lambda2); //this doesn't compile why?
      |                        ^
/home/insights/insights.cpp:3:24: note: function parameter 'c' with unknown value cannot be used in a constant expression
    3 |     constexpr auto k = c; 
      |                        ^
/home/insights/insights.cpp:3:24: note: in call to '(lambda at /home/insights/insights.cpp:13:30)(c)'
/home/insights/insights.cpp:2:23: note: declared here
    2 | constexpr auto func(T c) {
      |                       ^
7
  • 1
    IANALL, but I think your constexpr auto k = c; will fail to be a proper constexpr. If you just remove the constexpr so that the body of the template function is under the function constexpr instead of having another constexpr scope (that'll fail to be constexpr), you should be fine. I leave it to a language-lawyer can cite chapter-and-verse from the standard, and use the standard-ese terminology. Commented Mar 2, 2024 at 13:51
  • Do you want to (constexpr) copy the lambda, or call it constexpr auto k = c();? Commented Mar 2, 2024 at 14:03
  • Function parameters are NOT constexpr, so k = c would use non constexpr c. with capture, the member has to be copied (but it is not constexpr), whereas captureless lambda won't use this at all, and is still viable in constexpr. Commented Mar 2, 2024 at 14:09
  • @Jarod42 I want to understand how exactly the standard allows one to work but not the other. Also I am aware that function parameters are not constexpr. But this is for academic purposes only. Also note that clang still rejects the program of we call the c like in constexpr auto k= c(); while gcc and msvc accepts it. See demo I'll ask a separate question for that. Commented Mar 2, 2024 at 14:17
  • Simplified case Demo (without lambda). Commented Mar 2, 2024 at 14:26

1 Answer 1

3

Here is a similar, but simpler version of the code that doesn't compile:

constexpr auto func(int a) {
    constexpr auto b = a;
    return b;
};

int main() {
    constexpr int x = 1;
    constexpr int y = func(x);
}

Here, because x is constexpr, it's ok to read the value of x within the initialization of the constexpr variable y.

However, that's not good enough, because not only does y need to have a constant expression as its initializer, but so does b, which is also declared constexpr. And this is where the issue arises. If b had not been declared constexpr, then the code would be fine.

In order for constexpr auto b = ...; to be valid, the compiler has to be able to evaluate it as a constant expression in the context where it appears—not the larger enclosing context in which func is called. In the context where the initialization of b appears, the value of a is not known until runtime. func may be called in both constant expressions and non-constant expressions. So the check that b is a constant expression fails.

In the OP's program, it's very similar. lambda2, which captures i, is of a closure type having a data member of type const int ([expr.prim.lambda.capture]/10). When func is called, the statement

constexpr auto k = c;

in its body is instantiated, with c being a function parameter of that closure type. At this point, the initialization of k has to be checked as a constant expression in the context where it appears, not the context of the enclosing constant evaluation. The initialization uses the copy constructor of the closure type, which is defaulted ([expr.prim.lambda.closure]/15) and thus performs a memberwise-copy. It must therefore read the const int member of the function parameter c.

To give a bit more detail:

[dcl.constexpr]/6 states that "In any constexpr variable declaration, the full-expression of the initialization shall be a constant expression". Let's apply this to the declaration of k. In order for it to be a constant expression, it has to be a core constant expression that satisfies some other conditions ([expr.const]/14). [expr.const]/5 gives the conditions for an expression E to be a core constant expression. In this case E is the init-declarator of k.

When k is initialized, the const int data member of c will be read, as discussed above. This is an lvalue-to-rvalue conversion. [expr.const]/5.9 states that an lvalue-to-rvalue conversion is not allowed in a core constant expression E except when applied to either

  • a non-volatile glvalue that refers to an object that is usable in constant expressions, or
  • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E

The second bullet is clearly not satisfied: the lifetime of the const int member of c does not start within the evaluation of E (i.e. the init-declarator of k); it starts beforehand, when the function is called. As for the first bullet, a function parameter is not usable in constant expressions because it can never be both "constant-initialized" and "potentially-constant" ([expr.const]/4). In particular, a function parameter of scalar or reference type cannot satisfy [expr.const]/2.1 and a function parameter of class type cannot satisfy [expr.const]/3.

(By the way, even in a consteval function, a function parameter's value still isn't a constant expression, despite the fact that a consteval function can only be called during constant evaluation. There is no fundamental reason why it can't be supported in the consteval case, but if it were allowed then it would create implementation difficulties and turn almost every consteval function into an implicit template.)

Sign up to request clarification or add additional context in comments.

7 Comments

This doesn't explain why lambda which doesn't capture i works. I want to know how the standard allows lambda but not lambda2 .
Also the example you gave is not actually similar. Lambdas are separate c++ constructs and have additional rules that have to be followed. So comparing it with a non-lambda example is questionable. For example the lambda works but not lambda2.
@Alan If there's no capture, then there's no data member that gets accessed during copy construction. I believe I made it clear in my answer that the lvalue-to-rvalue conversion on the const int data member is the issue.
@BrianBi Sorry but can you tell me why is it that when there is no capture there isn't a data member. I mean lambda is using i in its body so how can it use i in its body without capturing it(like without having a data member)
@Alan One of the links to the standard that I provided explains when data members are declared in the closure type. If your question is "why am I allowed to use that variable without capturing it", that should be posted as a new question.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.