Apple’s Objective-C compiler allows users to freely mix C++ and Objective-C in the same source file, and the resulting language is called Objective-C++. Compared to other languages (e.g. Swift, Kotlin, Dart, etc.) that use file isolation and bridge communication with C++ (e.g. Kotlin uses JNI and Dart uses FFI), Objective-C and C++’s same-file mashup is certainly comfortable. Although OC/C++ mashups can be written in a single file, there are some caveats to understand: Objective-C++ does not add C++ functionality to OC classes, nor does it add OC functionality to C++, e.g., you cannot call C++ objects with OC syntax, nor can you add constructors and destructors to OC objects. Nor can this and self be used interchangeably. The class architecture is independent, C++ classes cannot inherit OC classes, and OC classes cannot inherit C++ classes.

This article explores the previously confusing issue of OC’s Block and C++’s lambda mix.

Experimental environment: C++ version is C++14, OC is limited to ARC only.

Basic Understanding

Before exploring in depth, understand the two by way of comparison.

Syntax

1
2
3
4
5
6
7
8
^(int x, NSString *y){} // ObjC, take int and NSString*
[](int x, std::string y){} // C++, take int and std::string

^{ return 42; } // ObjC, returns int
[]{ return 42; } // C++, returns int

^int { if(something) return 42; else return 43; }
[]()->int { if(something) return 42; else return 43; }

Principle

Here is not to do a deeper exploration of the bottom of the Block of OC, just to expand the code to achieve a comparative effect.

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
    [super viewDidLoad];

    int x = 3;
    void(^block)(int) = ^(int a) {
        NSLog(@"%d", x);
    };
    block(5);
}

By rewriting clang -rewrite-objc, you can get the following result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int x;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _x, int flags=0) : x(_x) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself, int a) {
   int x = __cself->x; // bound by copy
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_st_jhg68rvj7sj064ft0rznckfh0000gn_T_ViewController_d02516_mii_0, x);
}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    int x = 3;
    void(*block)(int) = ((void (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, x));
    ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 5);
}

C++ lambda takes a very different implementation mechanism and converts the lambda expression into an anonymous C++ class. Here is a look at the C++ lambda implementation with the help of cppinsights.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <cstdio>

struct A {
  int x;
  int y;
};

int main()
{
    A a = {1, 2};
    int m = 3;
    auto add = [&a, m](int n)->int {
        return m + n + a.x + a.y;
    };
    m = 30;
    add(20);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <cstdio>

struct A
{
    int x;
    int y;
};

int main()
{
    A a = {1, 2};
    int m = 3;

    class __lambda_12_15
    {
    public:
        inline int operator()(int n) const
        {
            return ((m + n) + a.x) + a.y;
        }

    private:
        A & a;
        int m;

    public:
        __lambda_12_15(A & _a, int & _m)
        : a{_a}
        , m{_m}
        {}
    };

    __lambda_12_15 add = __lambda_12_15{a, m};
    m = 30;
    add.operator()(20);
    return 0;
}

You can see that: the lambda expression add is converted to class __lambda_12_15 and the operator () is overloaded, and calls to add are converted to calls to add.operator().

Capturing variables

OC Block is only possible to capture variables in the normal way and in the __block way.

1
2
3
int x = 42;
void (^block)(void) = ^{ printf("%d\n", x); };
block(); // prints 42
1
2
3
__block int x = 42;
void (^block)(void) = ^{ x = 43; };
block(); // x is now 43

C++ lambda brings more flexibility to capture variables in these ways.

1
2
3
4
5
6
[] Capture nothing
[&] Capture any referenced variable by reference
[=] Capture any referenced variable by making a copy
[=, &foo] Capture any referenced variable by making a copy, but capture variable foo by reference
[bar] Capture bar by making a copy; don't copy anything else
[this] Capture the this pointer of the enclosing class
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int x = 42;
int y = 99;
int z = 1001;
auto lambda = [=, &z] {
    // can't modify x or y here, but we can read them
    z++;
    printf("%d, %d, %d\n", x, y, z);
};
lambda(); // prints 42, 99, 1002
// z is now 1002

Memory management

OC Block and C++ lambda both have their roots in stack objects, but their subsequent development is very different. OC Block are essentially OC objects, they are stored by reference, never by value. OC Blocks must be copied to the heap in order to extend their lifecycle. OC Blocks follow the OC reference counting rules, and copy and release must be balanced (same for Block_copy and Block_release). The first copy will move Block from the stack to the heap, and another copy will increase its reference count. When the reference count reaches 0, Block is destroyed and the object it captures is released.

C++ lambda stores by value, not by reference. All captured variables are stored in the anonymous class object as member variables of the anonymous class object. When lambda expressions are copied, all of these variables are copied as well, simply by triggering the appropriate constructors and destructors. There is an extremely important point here: variables are captured by reference. These variables are stored as references in anonymous objects and they don’t get any special treatment. This means that after the lifetime of these variables is over, lambda may still access them, resulting in undefined behavior or crashes, e.g.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 - (void)viewDidLoad {
    [super viewDidLoad];

    int x = 3;
    lambda = [&x]() -> void {
        NSLog(@"x = %d", x);
    };
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    lambda();
}

// 从输出结果中可以看到x是一个随机值
2022-02-12 23:15:01.375925+0800 BlockTest[63517:1006998] x = 32767

In contrast, this points to a store on the heap, which has a guaranteed lifetime, but even so, it is not absolutely guaranteed to be life-safe, and in some cases it is necessary to extend the lifetime with the help of smart pointers.

1
2
3
4
auto strongThis = shared_from_this();
doSomethingAsynchronously([strongThis, this]() {
    someMember_ = 42;
});

Closures mixed capture problem

The previous discussion is all independent of each other, OC’s Block does not involve C++ objects, and C++’s lambda does not involve OC objects, which is probably what we would like to see, but the mixup process will reveal that this is just wishful thinking on our part. The two tend to extend their magic wands into each other’s domain, which can lead to some rather puzzling problems.

C++’s lambda captures OC objects

Can C++’s lambda capture OC variables? If so, is there a circular reference problem? If there is a circular reference problem, how should I handle it?

value capture OC object

As the code shows, there is a C++ field cppObj in the OCClass class, and in the initialization method of OCClass, cppObj is initialized and its field callback is assigned a value. You can see that self is captured in lambda, which can be considered value capture according to the previous rules.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class CppClass {
public:
    CppClass() {
    }

    ~CppClass() {
    }
public:
    std::function<void()> callback;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@implementation OCClass {
    std::shared_ptr<CppClass> cppObj;
}

- (void)dealloc {
    NSLog(@"%s", __FUNCTION__);
}

- (instancetype)init {
    if (self = [super init]) {
        cppObj = std::make_shared<CppClass>();
        cppObj->callback = [self]() -> void {
            [self executeTask];
        };
    }
    return self;
}

- (void)executeTask {
    NSLog(@"execute task");
}
1
OCClass *ocObj = [[OCClass alloc] init];

Unfortunately, such a capture occurs by circular reference: the OCClass object ocObj holds cppObj, and cppObj holds ocObj via callback.

OCClass

Looking at the corresponding assembly code, you can see that the capture triggers the ARC semantics and automatically retain on self.

assembly

These lines of assembly code add a reference count to self.

1
2
3
0x10cab31ea <+170>: movq   -0x8(%rbp), %rdi
0x10cab31ee <+174>: movq   0x5e7b(%rip), %rax        ; (void *)0x00007fff2018fa80: objc_retain
0x10cab31f5 <+181>: callq  *%rax

Finally, looking at the parameters of the anonymous class, you can see that self is of type OCClass *, which is a pointer type.

self is of type OCClass *

Then it can be simply assumed that the capture pseudocode is as follows, and that the retain behavior occurs under ARC semantics.

1
2
3
4
__strong __typeof(self) capture_self = self;

// 展开
__strong OCClass * capture_self = self;

To solve the problem of circular references, __weak can be used.

1
2
3
4
5
cppObj = std::make_shared<CppClass>();
__weak __typeof(self) wself = self;
cppObj->callback = [wself]() -> void {
    [wself executeTask];
};

assembly

Looking at the assembly code again, I see that the previous objc_retain logic has disappeared and is replaced by objc_copyWeak.

Capture OC objects by reference

So is it possible to capture self by reference capture?

1
2
3
4
cppObj = std::make_shared<CppClass>();
cppObj->callback = [&self]() -> void {
    [self executeTask];
};

You can see that there is also no objc_retain logic in the assembly code.

objc_retain

Finally, looking at the parameters of the anonymous class, we can see that self is of type OCClass *&, which is a pointer reference type.

self is of type OCClass *&

You can see that reference capture does not retain self, and you can simply assume that the capture pseudocode is as follows, and no retainment behavior occurs under ARC semantics.

1
2
3
4
__unsafe_unretained __typeof(self)& capture_self = self;

// 展开
__unsafe_unretained OCClass *&capture_self = self;

When is the captured OC object released?

Take this code snippet as an example.

1
2
3
4
5
auto cppObj = std::make_shared<CppClass>();
OCClass2 *oc2 = [[OCClass2 alloc] init];
cppObj->callback = [oc2]() -> void {
    [oc2 class];
};

When is the captured OC object released?

You can see that std::function is destructed in the destructor of CppClass, and std::function releases the OC variable oc2 that it captures.

Conclusion

The essence of C++ lambda is to create an anonymous struct type to store captured variables. ARC will ensure that C++ struct types containing OC object fields follow ARC semantics:

  1. the constructor of the C++ structure initializes the OC object field to nil;
  2. when the OC object field is assigned a value, it releases the previous value and retain the new value (or copy if it is a block).
  3. when the destructor of a C++ struct is called, it release the OC object field.

C++ lambda captures OC objects by value or by reference.

  1. capturing OC objects by reference is equivalent to using __unsafe_unretained, which has lifecycle issues and is inherently dangerous and not recommended.
  2. value capture is equivalent to using __strong, which may cause circular references, so you can use __weak if necessary.

How does OC’s Block capture C++ objects?

Take a look at how OC’s Block captures C++ objects.

The HMRequestMonitor in the code is a C++ structure with WaitForDone and SignalDone methods that are mainly for synchronization.

1
2
3
4
5
6
7
8
struct HMRequestMonitor  {
public:
    bool WaitForDone() { return is_done_.get(); }
    void SignalDone(bool success) { done_with_success_.set_value(success); }
    ResponseStruct& GetResponse() { return response_; }
private:
    .....
};

The upload method uses the HMRequestMonitor object for the purpose of waiting for network request results synchronously (the code has been adjusted for typography).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
hermas::ResponseStruct HMUploader::upload(
const char* url,
const char* request_data,
int64_t len,
const char* header_content_type,
const char* header_content_encoding) {
    HMRequestModel *model = [[HMRequestModel alloc] init];
    ......

    auto monitor = std::make_shared<hermas::HMRequestMonitor>();
    std::weak_ptr<hermas::HMRequestMonitor> weakMonitor(monitor);
    DataResponseBlock block = ^(NSError *error, id data, NSURLResponse *response) {
        weakMonitor.lock()->SignalDone(true);
    };
    [m_session_manager requestWithModel:model callBackWithResponse:block];
    monitor->WaitForDone();
    return monitor->GetResponse();
}

Here, std::weak_ptr is used directly.

does not use __block

does not use __block does not use __block

The following conclusions can be drawn from experiments.

  1. C++ objects are captured by OC’s Block and by value passing. A breakpoint shows that the copy constructor of std::weak_ptr is called.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    template<class _Tp>
    inline
    weak_ptr<_Tp>::weak_ptr(weak_ptr const& __r) _NOEXCEPT
        : __ptr_(__r.__ptr_),
        __cntrl_(__r.__cntrl_)
    {
        if (__cntrl_)
            __cntrl_->__add_weak();
    }
    
  2. The weak reference count of monitor changes as follows.

    • Initialize monitor with weak_count = 1 ;
    • When initializing weakMonitor, weak_count = 2, which increases by 1.
    • After OC Block capture, weak_count = 4, increased by 2. By looking at the assembly code, there are 2 places.
      • weakMinotor was copied on the first capture, at line 142 of the assembly code.
      • weakMinotor is copied again when Block is copied from the stack to the heap, in assembly line 144.

Here we need to pay attention to: C++ weak_count is strange, its value = number of weak references + 1, the reason for this design is more complicated, please refer to: https://stackoverflow.com/questions/5671241/how-does-weak-ptr-work

If instead of using std::weak_ptr, std::shared_ptr is caught and its strong reference count is 3, the logic is the same as for std::weak_ptr above. (Essentially, std::shared_ptr and std::weak_ptr are both C++ classes)

1
2
3
4
std::shared_ptr<hermas::HMRequestMonitor> monitor = std::make_shared<hermas::HMRequestMonitor>();
DataResponseBlock block = ^(NSError * _Nonnull error, id  _Nonnull data, NSURLResponse * _Nonnull response) {
    monitor->SignalDone(true);
};
1
2
(lldb) po monitor
std::__1::shared_ptr<hermas::HMRequestMonitor>::element_type @ 0x00006000010dda58 strong=3 weak=1

using __block

So is it possible to use __block to modify a captured C++ variable? Experimentation has shown that it is possible.

using __block using __block

The following conclusions can be drawn.

  1. the Block of OC can capture C++ objects by reference passing.
  2. the weak reference count of monitor is as follows.
    • Initialize monitor with weak_count = 1 ;
    • Initialize weakMonitor with weak_count = 2, increasing by 1.
    • After OC Block capture, weak_count = 2, mainly because the move constructor is triggered, which is only a transfer of ownership and does not change the reference count.

using __block

Questions about __block

Those who know C++ may wonder, since the move constructor is triggered here, only the ownership has been transferred, meaning that monitor is passed in as the right value and has become nullptr to be extinguished, then why is monitor still accessible in the example? It can be verified that.

  1. When the following code is executed for the first time

    code

    will find the address of the monitor variable as

    1
    2
    
    (lldb) po &monitor
    0x0000700001d959e8
    
  2. When the assignment of block is executed, the move constructor of std::shared_ptr is called.

    std::shared_ptr std::shared_ptr

    • The address of this in the move constructor is 0x0000600003b0c830 ;
    • The address of __r is also 0x0000700001d959e8, which is the same as the address of monitor.
  3. When the execution of block is finished, print the address of monitor again, you will find that the address of monitor has changed and is consistent with this in step 2, which means that monitor has changed to this in step 2.

    1
    2
    
    (lldb) po &monitor
    0x0000600003b0c830
    

During the whole process, the address of monitor changes before and after 2 different std::shared_ptr objects. So monitor can still be accessed.

When is a captured C++ object released?

a captured C++ object released

Also when OC’s Block is released, it is released for the C++ object it captures.

captures shared_from_this

C++’s this is a pointer, which is essentially an integer. OC’s Block capturing this is not fundamentally different from capturing an integer, so we won’t discuss it in detail here. The focus here is on C++’s shared_from_this class, which is a smart pointer version of this.

If a C++ class wants to access shared_from_this, it must inherit from class enable_shared_from_this and pass its own class name as a template parameter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class CppClass : public std::enable_shared_from_this<CppClass> {
public:
    CppClass(){}
    ~CppClass() {}

    void attachOCBlock();
public:
    OCClass2 *ocObj2;
    void dosomething() {}
};

void CppClass::attachOCBlock() {
    ocObj2 = [[OCClass2 alloc] init];
    auto shared_this = shared_from_this();
    ocObj2.ocBlock = ^{
        shared_this->dosomething();
    };
}
1
2
3
@interface OCClass2 : NSObject
@property void (^ocBlock)();
@end
1
2
auto cppObj = std::make_shared<CppClass>();
cppObj->attachOCBlock();

According to the previous conclusion, in the CppClass member function attachOCBlock, ocBlock captures shared_from_this directly, which also triggers a circular reference, and also takes std::weak_ptr to resolve it.

1
2
3
4
5
6
7
void CppClass::attachOCBlock() {
    ocObj2 = [[OCClass2 alloc] init];
    std::weak_ptr<CppClass> weak_this = shared_from_this();
    ocObj2.ocBlock = ^{
        weak_this.lock()->dosomething();
    };
}

Conclusion

OC’s Block can capture C++ objects.

  1. if a C++ object on the stack is captured in the normal way, the copy constructor is called.
  2. If a C++ object on the stack is captured using the __block method, the move constructor is called, and the __block-modified C++ object is redirected when it is captured.

Summary

This article started with a brief comparison between OC’s Block and C++’s lambda in 4 dimensions: syntax, principles, variable capture and memory management, and then focused more on OC/C++’s closure hybrid capture. The reason why I went to such great lengths is that I don’t want to “guess” and “try and fail” in a confusing way, but only by understanding the mechanism behind it can I write better OC/C++ mixed code, and I also hope to bring some help to readers who have the same confusion. However, this is only the tip of the iceberg for the whole field of OC/C++ mashups, and there are still a lot of difficult issues to be explored in the future.