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
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
Experimental environment: C++ version is C++14, OC is limited to ARC only.
Before exploring in depth, understand the two by way of comparison.
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.
clang -rewrite-objc, you can get the following result.
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
You can see that: the
add is converted to class
__lambda_12_15 and the operator
() is overloaded, and calls to
add are converted to calls to
Block is only possible to capture variables in the normal way and in the
lambda brings more flexibility to capture variables in these ways.
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
release must be balanced (same for
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
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.
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.
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.
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
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.
Unfortunately, such a capture occurs by circular reference: the
Looking at the corresponding assembly code, you can see that the capture triggers the
ARC semantics and automatically
These lines of assembly code add a reference count to
Finally, looking at the parameters of the anonymous class, you can see that
self is of type
OCClass *, which is a pointer type.
Then it can be simply assumed that the capture pseudocode is as follows, and that the
retain behavior occurs under
To solve the problem of circular references,
__weak can be used.
Looking at the assembly code again, I see that the previous
objc_retain logic has disappeared and is replaced by
Capture OC objects by reference
So is it possible to capture
self by reference capture?
You can see that there is also no
objc_retain logic in the assembly code.
Finally, looking at the parameters of the anonymous class, we can see that
self is of type
OCClass *&, which is a pointer reference type.
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
When is the captured OC object released?
Take this code snippet as an example.
You can see that
std::function is destructed in the destructor of
std::function releases the OC variable oc2 that it captures.
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
- the constructor of the C++ structure initializes the OC object field to
- when the OC object field is assigned a value, it
releasesthe previous value and
retainthe new value (or
copyif it is a
- when the destructor of a C++ struct is called, it
releasethe OC object field.
lambda captures OC objects by value or by reference.
- capturing OC objects by reference is equivalent to using
__unsafe_unretained, which has lifecycle issues and is inherently dangerous and not recommended.
- value capture is equivalent to using
__strong, which may cause circular references, so you can use
How does OC’s Block capture C++ objects?
Take a look at how OC’s
Block captures C++ objects.
HMRequestMonitor in the code is a C++ structure with
SignalDone methods that are mainly for synchronization.
upload method uses the
HMRequestMonitor object for the purpose of waiting for network request results synchronously (the code has been adjusted for typography).
std::weak_ptr is used directly.
does not use
The following conclusions can be drawn from experiments.
C++ objects are captured by OC’s
Blockand by value passing. A breakpoint shows that the copy constructor of
The weak reference count of
monitorchanges as follows.
weak_count = 1;
- When initializing
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.
weakMinotorwas copied on the first capture, at line 142 of the assembly code.
weakMinotoris copied again when
Blockis copied from the stack to the heap, in assembly line 144.
Here we need to pay attention to: C++
weak_countis 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::shared_ptr is caught and its strong reference count is 3, the logic is the same as for
std::weak_ptr above. (Essentially,
std::weak_ptr are both C++ classes)
So is it possible to use
__block to modify a captured C++ variable? Experimentation has shown that it is possible.
The following conclusions can be drawn.
Blockof OC can capture C++ objects by reference passing.
weakreference count of
monitoris as follows.
weak_count = 1;
weak_count = 2, increasing by 1.
- After OC
weak_count = 2, mainly because the move constructor is triggered, which is only a transfer of ownership and does not change the reference count.
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.
When the following code is executed for the first time
will find the address of the monitor variable as
When the assignment of
blockis executed, the move constructor of
- The address of
thisin the move constructor is
- The address of
0x0000700001d959e8, which is the same as the address of
- The address of
When the execution of
blockis finished, print the address of
monitoragain, you will find that the address of
monitorhas changed and is consistent with
thisin step 2, which means that
monitorhas changed to
thisin step 2.
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?
Also when OC’s
Block is released, it is released for the C++ object it captures.
this is a pointer, which is essentially an integer. OC’s
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_thisand pass its own class name as a template parameter.
According to the previous conclusion, in the
CppClass member function
shared_from_this directly, which also triggers a circular reference, and also takes
std::weak_ptr to resolve it.
Block can capture C++ objects.
- if a C++ object on the stack is captured in the normal way, the copy constructor is called.
- If a C++ object on the stack is captured using the
__blockmethod, the move constructor is called, and the
__block-modified C++ object is redirected when it is captured.
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.