Lambda const capture by value bug in msvc?

Viewed 72

This code fails to compile with MSVC but compiles with GCC and CLANG. The problem appears to be that capture by value is not a compile time const in MSVC.

#include <array>
int main()
{
    const int n = 41;
    auto lambda = [n] {  // Both n and nn should be compile time const
        const int nn {n+1};
        std::array<int,nn> h{};
        return h;
    };
    auto xarray = lambda();
    return xarray.size();
}

Error: error C2971: 'std::array': template parameter '_Size': 'nn': a variable with non-static storage duration cannot be used as a non-type argument

Compiler Explorer

Here's a C++20 standard discussion of lambda value capture showing compile time evaluation:

//[Example 6:
void f1(int i) {
  int const N = 20;
  auto m1 = [=]{
    int const M = 30;
    auto m2 = [i]{
      int x[N][M];          // OK: N and M are not odr-used
      x[0][0] = i;          // OK: i is explicitly captured by m2 and implicitly captured by m1
    };
  };
};

So this is a MSVC bug? Sure looks like it to me.

2 Answers

MSVC is wrong in rejecting the code(1st snippet) because n is a compile time constant and so nn is also a compile time constant as it is initialized with n.

Note that n inside the lambda body refers to the n outside the lambda and not the member of the closure type because that member is unnamed and isn't ord-used. This is also described here Why visual c++ (latest) and gcc 12.1 accepted hiding this init capture for lambda, while clang 14.0.0 not? (c++20).

Thus, n is a compile time constant and so is usable in constexpr context. That is, std::array<int, nn> h{}; is valid because nn is a compile time constant as it was initialized by another compile time constant n.


Here is the msvc bug report:

MSVC rejects valid code involving use of constexpr variable as initializer

[expr.prim.lambda]p11:

Every id-expression within the compound-statement of a lambda-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type.
[Note 7: An id-expression that is not an odr-use refers to the original entity, never to a member of the closure type. However, such an id-expression can still cause the implicit capture of the entity. — end note]

So, in const int nn {n+1};, if n is odr-used then it refers to the member of the lambda (which obviously isn't useable in a constant expression), and if it's not an odr-use, then it refers to the entity in main.

This isn't an ODR-use, because [basic.def.od]p4:

A variable x whose name appears as a potentially-evaluated expression E is odr-used by E unless

  • [...]
  • 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
  • [...]

And n is usable in a constant expression. The n in the lambda refers to the n in main, and not the non-static data member of lambda that was initialized with 41, so it should work.

It looks like MSVC first checks if a variable is ODR-used and then substitutes all instances with the member access if it is (even where it is not ODR-used). The following also fails to compile with MSVC:

#include <array>

int main() {
    constexpr int n = 41;
    auto lambda = [=] {
        std::array<int, n> h{};
        return h;
        int i = *&n;  // ODR-use of n. Compiles if line is removed.
    };
}

This one also fails to compile for I suspect similar reasons:

    constexpr int n = 41;
    auto lambda = [n] {  // ODR-use of n to initialize the member
        std::array<int, n> h{};  // Not an ODR-use
        return h;
    };
Related