Home > Back-end >  Why can I "captureless-capture" an int variable, but not a non-capturing lambda?
Why can I "captureless-capture" an int variable, but not a non-capturing lambda?

Time:04-22

The following function is valid (as of C 20):

void foo() {
    constexpr const int b { 123 };
    constexpr const auto l1 = [](int a) { return b * a; };
    (void) l1;
}

even though l1 does not capture anything, supposedly, it is still allowed to "captureless-capture" the value of b, as it is a const (it doesn't even have to be constexpr; but see @StoryTeller's comment).

But if I try to capture something more complex in a new lambda:

void foo() {
    constexpr const int b { 123 };
    constexpr const auto l1 = [](int a) { return b * a; };
    (void) [](int c) { return l1(c) * c; };
}

This fails to compile. Why? The compiler should have no trouble calling l1 from within the lambda; so why is b ok for captureless-capture, and l1 isn't?

See this on GodBolt.

CodePudding user response:

This has to do with odr-use.

First, from [basic.def.odr]/10:

A local entity is odr-usable in a scope if:

  • either the local entity is not *this, or an enclosing class or non-lambda function parameter scope exists and, if the innermost such scope is a function parameter scope, it corresponds to a non-static member function, and
  • for each intervening scope ([basic.scope.scope]) between the point at which the entity is introduced and the scope (where *this is considered to be introduced within the innermost enclosing class or non-lambda function definition scope), either:
    • the intervening scope is a block scope, or
    • the intervening scope is the function parameter scope of a lambda-expression that has a simple-capture naming the entity or has a capture-default, and the block scope of the lambda-expression is also an intervening scope.

If a local entity is odr-used in a scope in which it is not odr-usable, the program is ill-formed.

So in this example, a is odr-usable but b is not:

void foo() {
    constexpr const int b { 123 };
    constexpr const auto l1 = [](int a) { return b * a; };
    (void) l1;
}

And in this example, similarly, the a and c are odr-usable, but neither b or nor l1 are.

void foo() {
    constexpr const int b { 123 };
    constexpr const auto l1 = [](int a) { return b * a; };
    (void) [](int c) { return l1(c) * c; };
}

But the rule isn't just "not odr-usable", it's also "odr-used". Which one(s) of these are odr-used? That's [basic.def.odr]/5:

A variable is named by an expression if the expression is an id-expression that denotes it. A variable x whose name appears as a potentially-evaluated expression E is odr-used by E unless

  • x is a reference that is usable in constant expressions ([expr.const]), or
  • x is a variable of non-reference type that is usable in constant expressions and has no mutable subobjects, and E is an element of the set of potential results of an expression of non-volatile-qualified non-class type to which the lvalue-to-rvalue conversion ([conv.lval]) is applied, or
  • x is a variable of non-reference type, and E is an element of the set of potential results of a discarded-value expression ([expr.context]) to which the lvalue-to-rvalue conversion is not applied.

For the b * a case, b is "a variable of non-reference type that is usable in constant expressions" and what we're doing with it is applying "the lvalue-to-rvalue conversion". That's the second bullet exception to the rule, so b is not odr-used, so we don't have the odr-used but not odr-usable problem.

For the l1(c) case, l1 is also "a variable of non-reference type that is usable in constant expressions"... but we're not doing an lvalue-to-rvalue conversion on it. We're invoking the call operator. So we don't hit the exception, so we are odr-using l1... but it's not odr-usable, which makes this ill-formed.

The solution here is to either capture l1 (making it odr-usable) or make it static or global (making the rule irrelevant since l1 wouldn't be a local entity anymore).

  • Related