Defer footgun or skill issue? (spoiler: skill issue)

I have some code where I iterate an array of structs, capturing a pointer to the element. Each element contains a start and length field. The length is used to increase the value of an accumulator.

In order to accomplish this, I have a few options:

  1. store the previous iteration’s accumulator value then bump it immediately
  2. use the accumulator value directly then bump it at the end of the loop
  3. use defer to accomplish option 2 while maintaining the locality of statements in option 1

Here’s how those play out:

Option 1

var acc: usize = 0;
for (items) |*item| {
    const pos = acc;
    acc += item.len;

    // do stuff with pos
}

Option 2

var acc: usize = 0;
for (items) |*item| {
    // do stuff with acc

    acc += item.len;
}

Option 3

var acc: usize = 0;
for (items) |*item| {
    defer acc += item.len;

    // do stuff with acc
}

Stylistically, I prefer option 3 but I discovered a not-so-obvious problem with this. Option 3 was segfaulting for me, and the best guess at why is because the pointer no longer referenced valid memory (something to do with this “at scope exit”). I don’t know why, though.

To fix this, I store a copy of the dereferenced value in the loop and use that instead of the capture:

var acc: usize = 0;
for (items) |*item| {
    const item_copy = item.*;
    defer acc += item_copy.len;

    // do stuff with acc
}

So, is this a skill issue resulting from a misunderstanding of defer? Or is this a possible footgun? The langref says this about defer:

Executes an expression unconditionally at scope exit.

To me, this is a bit vague. What does “at scope exit” mean (in a temporal sense)?

EDIT: The segfault was occurring in a test.

I cannot reproduce a segfault, it may have something to do with the contents of items or the rest of the loop body…

1 Like

The segfault was occurring in the tests, not in main. I have updated my post.

I’ll see if I can get a MRE.

I don’t see any reason to prefer option 3 over 2 here, though I realize that’s irrelevant to the actual question.

This’ll certainly be interesting if it’s reproducible..

Spot on.

This was a skill issue. I made the classic, bone-headed mistake of inserting an item into the list while iterating it. The list reallocated to grow and the captured pointer was no longer valid.

7 Likes

Sometimes the loop body can get long and I forget it’s down there (out of sight, out of mind). If the loop gets refactored to include some early returns, for example, the accumulator doesn’t get updated and the next iteration is indexing a buffer at the wrong position.

I don’t know, maybe it’s just me, but I like to do the “bookkeeping” up front and center.

You make a good point, if the loop had early returns it’s a good place for it!

If you know length of the loop, and you know since its for loop. You can always insert ensureUnusedCapacity statement before the loop and do appending inside just fine since it wont resize

2 Likes

Rust would never!

Incrementing an index in a defer clause is generally incorrect, as you wouldn’t want to perform that action following a break or a return.

If the loop body gets long, move the code into a function.

1 Like