Archive for October, 2024

Tracking Down Compiler Issues with C-Reduce

Monday, October 14th, 2024

When I first came across the issue described in the previous post, I was baffled. I tried to reduce the problem, but whenever I tried to manually copy out bits I thought could be the problem, the problem went away. The “building up” solution not working, I started to try at the “breaking down” solution, starting from the full code that demonstrated the problem and deleting irrelevant code. But this was in a file more than a thousand lines long, and the broken code had many dependencies on other parts of this file, making stripping it down difficult.

But I had a tool in my toolbox that could help. I’ve been a fan of delta debugging for a long time. With delta debugging, you define an “interestingness” criterion, and provide this, along with a large but interesting input to a reducer tool, and it will attempt to reduce the code to the smallest example that is still interesting per the provided criterion.

(more…)

I Can’t Destroy my Lambda?

Monday, October 7th, 2024

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.