2

I've been playing with blocks and encountered a weird behavior. This is the interface/implementation, which just holds a block with the ability to execute it:

@interface TestClass : NSObject {
#if NS_BLOCKS_AVAILABLE
    void (^blk)(void);
#endif
}
- (id)initWithBlock:(void (^)(void))block;
- (void)exec;
@end

@implementation TestClass
#if NS_BLOCKS_AVAILABLE
- (id)initWithBlock:(void (^)(void))block {
    if ((self = [super init])) {
        blk = Block_copy(block);
    }
    return self;
}
- (void)exec {
    if (blk) blk();
}
- (void)dealloc {
    Block_release(blk);
    [super dealloc];
}
#endif
@end

While a regular instantiation and passing a regular block works:

TestClass *test = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass");
}];
[test exec];
[test release];

Using a block with reference to the object which is being created doesn't:

TestClass *test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];

Error is EXC_BAD_ACCESS, stack trace on Block_copy(block); Debugger on: 0x000023b2 <+0050> add $0x18,%esp

I kept playing around, and moved the allocation code above the initialization, it worked:

TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
    NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];

And combining both snippets works too:

TestClass *test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];

TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
    NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];

What's going on here?

Paul Sweatte
  • 24,148
  • 7
  • 127
  • 265
elado
  • 8,510
  • 9
  • 51
  • 60

2 Answers2

5

In an assignment expression, the rvalue is evaluated before being assigned to the lvalue.

This means that in:

TestClass *test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];

the following sequence of operations is performed. Edit: as pointed out by Jonathan Grynspan, there’s no defined order for steps 1 and 2 so it could be the case that step 2 is executed before step 1.

  1. Send +alloc to TestClass
  2. Create a block that refers to test1, which hasn’t been initialised yet. test1 contains an arbitrary memory address.
  3. Send -initWithBlock: to the object created in step 1.
  4. Assign the rvalue to test1.

Note that test1 points to a valid object only after step 4.

In:

TestClass *test2 = [TestClass alloc];
test2 = [test2 initWithBlock:^{
    NSLog(@"TestClass %@", test2);
}];
[test2 exec];
[test2 release];

the sequence is:

  1. Send +alloc to TestClass
  2. Assign the rvalue to test2, which now points to a TestClass object.
  3. Create a block that refers to test2, which points to the TestClass object per step 2.
  4. Send -initWithBlock: to test2, which was correctly assigned in step 2.
  5. Assign the rvalue to test2.
  • 2
    Actually, the order of evaluation between `[TestClass alloc]` and `^{ ... }` is undefined. At the C level, both are evaluated as arguments to `-initWithBlock:`, so the block could actually be created *before* `[TestClass alloc]` is called. :) – Jonathan Grynspan May 18 '11 at 23:20
  • Thanks, that makes sense, this is why I tried to split the statements. But how come it works when both non-working snippet and working snippet are combined? – elado May 18 '11 at 23:26
  • @elado It doesn’t, at least not reliably. It depends on what `test1` contains before the assignment, which is undefined. –  May 18 '11 at 23:31
  • @elado Also, you’re not returning `self` in your `-init…` method. –  May 18 '11 at 23:31
  • @Bavarious I fixed the return self. It did fix the odd situation where the two snippets were running ok. The mystery on the EXC_BAD_ACCESS on the closure before alloc is solved. Thank you guys! – elado May 19 '11 at 00:01
  • It's also worth noting that in the second case, it only "works" in this particular case because `-[TestClass initWithBlock:]` returns the object it is called on. But initializers are not guaranteed to return the object it is called on; they can return `nil` or another object (in the latter case they would release the object they were called on, and retain the one they return). If `-[TestClass initWithBlock:]` had returned a different object, then the same problem as the first case would occur. – newacct Aug 21 '14 at 22:51
1

The problem is that when a block is created, it will copy (make a separate copy) of any non-__block variables it captures. Since test1 is uninitialized at the time your block is created, you will be using an uninitialized pointer for test1 when you run the block.

The proper solution is to declare test1 with __block. That way, state is shared between the block and the enclosing scope, and after test1 is assigned in the enclosing scope, the block can access the changed value:

__block TestClass *test1;
test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", test1);
}];
[test1 exec];
[test1 release];

p.s. The 3rd example (doing alloc before and then assigning the result of init) is not reliable because in general an object's init method is not guaranteed to return the object it is called on (init is allowed to deallocate itself and return nil if it failed, for example).


Update: The above code is only for MRC, as __block variables are not retained by the block.

But in ARC, the above code will cause a retain cycle, as __block object pointer variables are by default retained by the block. In ARC, the correct code is:

TestClass *test1;
__block __weak TestClass *weakTest1;
weakTest1 = test1 = [[TestClass alloc] initWithBlock:^{
    NSLog(@"TestClass %@", weakTest1);
}];
[test1 exec];
user102008
  • 30,736
  • 10
  • 83
  • 104