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
|
|
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.
By rewriting clang -rewrite-objc
, you can get the following result.
|
|
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
.
|
|
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.
C++ lambda
brings more flexibility to capture variables in these ways.
|
|
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.
|
|
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.
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.
|
|
|
|
Unfortunately, such a capture occurs by circular reference: the OCClass
object ocObj
holds cppObj
, and cppObj
holds ocObj
via callback
.
Looking at the corresponding assembly code, you can see that the capture triggers the ARC
semantics and automatically retain
on self
.
These lines of assembly code add a reference count to self
.
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 ARC
semantics.
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 objc_copyWeak
.
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 ARC
semantics.
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 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:
- the constructor of the C++ structure initializes the OC object field to
nil
; - when the OC object field is assigned a value, it
releases
the previous value andretain
the new value (orcopy
if it is ablock
). - 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.
- 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__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.
The upload
method uses the HMRequestMonitor
object for the purpose of waiting for network request results synchronously (the code has been adjusted for typography).
|
|
Here, std::weak_ptr
is used directly.
does not use __block
The following conclusions can be drawn from experiments.
-
C++ objects are captured by OC’s
Block
and by value passing. A breakpoint shows that the copy constructor ofstd::weak_ptr
is called. -
The weak reference count of
monitor
changes as follows.- Initialize
monitor
withweak_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 whenBlock
is copied from the stack to the heap, in assembly line 144.
- Initialize
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)
using __block
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.
- the
Block
of OC can capture C++ objects by reference passing. - the
weak
reference count ofmonitor
is as follows.- Initialize
monitor
withweak_count = 1
; - Initialize
weakMonitor
withweak_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.
- Initialize
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.
-
When the following code is executed for the first time
will find the address of the monitor variable as
-
When the assignment of
block
is executed, the move constructor ofstd::shared_ptr
is called.- The address of
this
in the move constructor is0x0000600003b0c830
; - The address of
__r
is also0x0000700001d959e8
, which is the same as the address ofmonitor
.
- The address of
-
When the execution of
block
is finished, print the address ofmonitor
again, you will find that the address ofmonitor
has changed and is consistent withthis
in step 2, which means thatmonitor
has changed tothis
in 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.
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 classenable_shared_from_this
and pass its own class name as a template parameter.
|
|
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.
Conclusion
OC’s 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
__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.