I Can’t Destroy my Lambda?

Or so I asked, when I saw this error message from GCC:

error: use of deleted function ‘foo()::<lambda(auto:1)>::~<lambda>()
note: ‘foo()::<lambda(auto:1)>::~<lambda>()’ is implicitly deleted because the default definition would be ill-formed

That’s sure odd. This same code compiled fine in Clang, and I didn’t think I was doing anything terribly unusual. The code was attempting to assemble a string from pieces, as I had just posted about. This time, our a and b are a little different. In this case, b was a fixed length, and I applied some transformation to a copy before putting it in the string. Since I know the contents of our writeWith lambda will be executed twice, I want to put the copy and transform outside of the lambda, like so:

void foo(std::string_view a, const std::array<char, 16> &b) {
    char c[b.size()];
    std::memcpy(c, b.data(), b.size());
    rot13(c, sizeof(c));
    auto writeWith = [&](auto write) {
        write("a: "sv);
        write(a);
        write(", b: "sv);
        write(std::string_view(c, sizeof(c)));
    };
    // ...
}

Should be hunky-dory, right? Well, kind of – it works on Clang. But on GCC 10.3.0 which I was using, it doesn’t, giving us the same error mentioned earlier, with the “use of deleted function”/“default definition would be ill-formed”.

After some minimizing (subject of another post), we determine that even this causes the behavior discrepancy:

void foo(std::array<char, 16> &a) {
    char b[a.size()];
    [&](auto) { b; };
}

That’s a little bit weird, but it doesn’t seem wrong. I tried putting it in Compiler Explorer, and… it worked. That’s weird. Tweak the GCC version. It does fail on 10.3.0. Poking through the versions of GCC, it fails in every GCC version 13 and below, and compiles without error in GCC 14 and up.

In situations like these, I also like to see what MSVC does. In this case, we do get an error, but it complains about something else:

error C2131: expression did not evaluate to a constant
note: failure was caused by a read of a variable outside its lifetime
note: see usage of ‘b

Now why would that be? size() is a constexpr function, it should be able to figure this out at compile-time, right?

Not quite. constexpr functions can access data members of the instance they’re called on, but the instance it’s called on would also have to be known at compile-time for the result to be a compile-time-constant value. In this case, the std::array reference is passed in, so the compiler doesn’t know its value. std::array::size doesn’t actually use any of the data members – it returns a value that could be known at compile time – but that doesn’t change the semantics that calling a.size() is not technically a compile-time constant.

When a.size() is not compile-time constant, that means b is a variable-length array. Variable-length arrays are not supported in C++, but GCC and Clang support them as an extension. MSVC does not support them, so MSVC issues an error. Aha. So older versions of GCC must have some bug regarding capturing a VLA into a lambda.

This is not hard to fix. We’ve got a number of options, but they all center around replacing a.size() with something actually compile-time-constant:

  • a.size() could be replaced with just a hard-coded ‘16’.
  • a.size() could be replaced with sizeof(a), since the array is of char.
  • a.size() could be replaced with sizeof(a) / sizeof(a[0]), which would work even for non-char element types.
  • A template specialization could be used to extract the array size:
template <typename> struct ArraySize;
template <typename El, std::size_t Sz>
struct ArraySize<std::array<El, Sz>> {
    static constexpr std::size_t size = Sz;
};
template <typename T>
static inline constexpr int arraySizeOf = ArraySize<std::decay_t<T>>::size;

void foo(std::array<char, 16> &a) {
    char b[arraySizeOf<decltype(a)>];
    [&](auto) { b; };
}

Any of these avoid the error, and in fact make the code compile on MSVC as well as all relevant versions of GCC and Clang.

To avoid this problem in the future, I’ve decided to compile all code with -Werror=vla. I intend my code to work on MSVC, so my code cannot contain VLAs if it is to do so. This allows me to detect this issue earlier, since I only occasionally compile with MSVC and it is better if all the compilers catch my errors early.

Tags: ,

Leave a Reply