Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rules for variable capture by block in objective-C

What are the semantics of capturing a variable by a block in objective-C?

#import <Foundation/Foundation.h>

#include <stdio.h>

int main()
{
  NSMutableArray *arr = [NSMutableArray array];
  for (int i = 0; i < 100; ++i) {
    int j = i;
    [arr addObject:^(void) {printf("%d %d\n", i, j); }];
  }
  for (void (^blk)(void) in arr) {
    blk();
  }
}

I was expecing this to print something like:

100 0
100 1
...
100 99

Instead, it prints:

99 99
99 99
...
99 99

How is it even possible that it's interpreting j as equal to 99 ? j isn't even alive outside the for loop.

like image 358
Brennan Vincent Avatar asked May 11 '16 23:05

Brennan Vincent


2 Answers

Because you're not using ARC! Without it, your block isn't being copied. You're just getting lucky and running the very last block every single time.

like image 176
ryanrhee Avatar answered Oct 21 '22 21:10

ryanrhee


The reason you're seeing 99 99 many times is simply due to undefined behaviour.

Let's take the first for-loop:

for (int i = 0; i < 100; ++i) {
  int j = i;
  dispatch_block_t block = ^(void) {printf("%d %d\n", i, j); };
  [arr addObject:block];
}

[I've pulled out the block for clarity.]

Inside this for-loop, the block is created. It is created on the stack and never moved to the heap because there is no copy of the block to do so.

Each time around the for-loop, it's extremely likely (well, certain really) that the same stack space is used for the block. And then the address of the block (which is on the stack) is added to arr. The same address each time. But it's a new implementation of the block each time.

Once out of the first for-loop, arr contains the same value 100 times. And that value points to the last created block, which is still on the stack. But it's pointing to a block on the stack that can no longer be accessed safely because it's out of scope.

In the case of this example however, the stack space occupied by the block by sheer luck (OK, simple code) hasn't been reused. So when you go and use the block, it "works".

The correct solution is to copy the block when it is added to the array. Either by calling copy or letting ARC do that for you. That way, the block is copied to the heap and you have a reference counted block that will live as long as needed by the array and the scope in which it is created.

If you want to learn more about how blocks work and understand this answer more deeply, then I suggest my explanations here:

http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-1/ http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/ http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/

like image 5
mattjgalloway Avatar answered Oct 21 '22 22:10

mattjgalloway