Posts Tagged ‘C++’

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.

Constructing Strings from Pieces Without a Dynamic Buffer

Monday, September 30th, 2024

Say I want to make a string out of a couple different pieces. Say we’ve got some input strings a and b and I want to make some kind of debug description, like maybe a: <a>, b: <b>. Shouldn’t be too hard, right? Slap it together:

std::string result = "a: " + a + ", b: " + b;

Easy, right? How many allocations does this incur, though? It could be more than one. We can do better than that – we know up-front everything going into it, so surely we can allocate the right amount of space up-front. How would that look?

(more…)

Locking when No One’s Looking

Monday, September 16th, 2024

You’re getting ready to start a thread. But wait! There are some variables you need to get ready before then. Those variables are read by the thread. How do you know the new values of those variables will be visible to the thread?

var = 1;
thread = std::thread([this] {
    use(var);  // Will I see "1"?
});

One conservative approach might be to obtain a lock while modifying those variables. This would make a lot of sense, particularly if those variables are typically guarded by that lock during the lifetime of the thread. At the same time, it does feel a little silly obtaining a lock that you know will never be contended, as the only other thing that would lock that mutex has not started yet.

{
    std::lock_guard lock(mutex);
    var = 1;
}
thread = std::thread([this] {
    std::lock_guard lock(mutex);  // Even without this
    use(var);
});

Modifying those variables under a lock is for-sure safe: releasing the lock after modifying will do a “release” memory barrier, ensuring all writes done will be visible. The thread reading them has not started yet, so there is no possible way anything could be reordered too early to see the release. If the thread also locks the mutex before reading those values, it is even more obviously safe.

But is any of that really needed? Intuitively it shouldn’t be needed, but memory models are notoriously tricky things, so let’s check our assumptions.

(more…)

MSVC Trickiness: CRTP Dependencies

Monday, September 2nd, 2024

I got complacent developing my game on a Mac and Linux boxes so far, and have neglected to actually compile my game on Windows, even though I from the start did intend Windows as a first-class deployment target. I’ve finally gotten around to it, and to no surprise, it’s not been particularly smooth. But some of the problems have been surprising to me.

One I had not previously encountered is this: One portion of my game uses a CRTP (Curiously Recurring Template Pattern) base, that abstracts away some repetitive bits that several derived types need. It looked something like this:

struct Meta {
    const char *stuff;
};

template <typename Derived>
struct Helper {
    static constexpr Meta kMeta = { Derived::kStuff };
};

struct Foo : Helper<Foo> {
    static constexpr const char *kStuff = "stuff";
};

This works fine on GCC, Clang, and ICC. But MSVC?

error C2039: 'kStuff': is not a member of 'Derived'

No way!

(more…)

Sort Out Your PODs

Monday, July 22nd, 2024

Some time ago I was helping a friend debug a crash in an application they were working on. It worked fine in production, but when running it on a smaller test file, it kept crashing. However, the crash was always on exit – the application itself seemed to run without issue. The crash was from a free, complaining about the pointer:

free(): invalid pointer
Aborted

GDB showed the offending free was from compiler-generated code, with no user code on the stack:

(more…)