📜 ⬆️ ⬇️

Objective-c blocks and c ++ lambdas

I hope that the post will be useful to people who are familiar with C ++ lambdas, but want to learn Objective-C blocks and vice versa.
Here I tried to describe the syntax of closures, context capture mechanisms, memory management, and the interaction of lambdas and blocks with each other.
All examples used Apple LLVM Compiler 4.2 (Clang). For the given Obj-C code, ARC is not used, because I hold the opinion that it is necessary to know how non-ARC code works in order to understand how ARC works.

Sections:


  1. Syntax
  2. Context capture
  3. Memory management
  4. Objective-C ++

Blocks in Objective-C is the implementation of closures [2] . Blocks are anonymous functions that can capture a context (current stack variables and member variables of classes). Blocks at run time are represented by objects; they are analogous to lambda expressions in C ++.

Lambda in C ++ are also implementations of closures and are unnamed local functions.

Syntax



Obj-c blocks

[3]
')
In text form
int multiplier = 7; int (^myBlock)(int) = ^(int num) { return num * multiplier;}; NSLog(@”%d”, myBlock(4)); //  28 

  • ^ - this symbol tells the compiler that the variable is a block
  • int - the block takes one parameter of type int, and returns a parameter of type int
  • multiplier - a variable that comes to us from the context (more on this in the section “Capture Context”)


Blocks have pointer semantics.
Blocks in Objective-C have already firmly found their application both in standard frameworks (Foundation, UIKit) and in third-party libraries ( AFNetworking , BlocksKit ).
Let's give an example in the form of a category class NSArray

An example of using a block in NSArray
 //   @implementation NSArray (Blocks) //   ,     - (NSArray*)subarrayWithPredicate:(BOOL(^)(id object, NSUInteger idx, BOOL *stop))predicte { NSMutableArray *resultArray = [NSMutableArray array]; BOOL shouldStop = NO; for (id object in self) { if (predicte(object, [self indexOfObjectIdenticalTo:object], &shouldStop)) { [resultArray addObject:object]; } if (shouldStop) { break; } } return [[resultArray copy] autorelease]; } @end // -    NSArray *numbers = @[@(5), @(3), @(8), @(9), @(2)]; NSUInteger divisor = 3; NSArray *divisibleArray = [numbers subarrayWithPredicate:^BOOL(id object, NSUInteger idx, BOOL *stop) { BOOL shouldAdd = NO; //     3 NSAssert([object isKindOfClass:[NSNumber class]], @"object != number"); //  ,   divisor     if ([(NSNumber *)object intValue] % divisor == 0) { shouldAdd = YES; } return shouldAdd; }]; NSLog(@"%@", divisibleArray); //  3, 9 


First of all, they are great for asynchronous operations, this can be seen using AFNetworking, and working with them in GCD is a pleasure.
We can define our block types, for example:

Block type declaration
 typedef int (^MyBlockType)(int number, id object); 


C ++ lambda

The same code is in the form of lambda
[eleven]
In text form
 int multiplier = 7; auto lambda = [&multiplier](int num) throw() -> int { return multiplier * num; }; lambda(4); //  28 

  • [] - the beginning of the declaration of lambda, inside - capture context
  • &multiplier - the captured variable ( & means it is captured by reference)
  • int - input parameter
  • mutable - a keyword that allows you to modify variables captured by value
  • throw() - means that lambda does not throw any exceptions out


Let's give a similar example of selecting a subset for a lambda.

Retrieving a Subset from the Predicate Collection
 template<class InputCollection, class UnaryPredicate> void subset(InputCollection& inputCollection, InputCollection& outputCollection, UnaryPredicate predicate) { typename InputCollection::iterator iterator = inputCollection.begin(); for (;iterator != inputCollection.end(); ++iterator) { if (predicate(*iterator)) { outputCollection.push_back(*iterator); } } return; } int main(int argc, const char * argv[]) { int divisor = 3; std::vector<int> inputVector = {5, 3, 8, 9, 2}; std::vector<int> outputVector; subset(inputVector, outputVector, [divisor](int number){return number % divisor == 0;}); //     std::for_each( outputVector.begin(), outputVector.end(), [](const int& number){std::cout << number << std::endl;} ); } 



Context capture


Obj-c blocks

We can automatically use the values ​​of stack variables in blocks, if we do not change them. For example, in the example above, we did not indicate in the block declaration that we wanted to capture the multiplier variable (unlike lambda, in lambda we could specify [&] to capture the entire context by reference, or [=] to capture the entire context by value).
We simply took its value by the name declared in the function body. If we wanted to change its value in the body of the block, we would have to mark the variable with the __block modifier

An example of changing the value of a variable from the context
 __block int first = 7; void (^myBlock2)(int) = ^(int second) { first += second;}; myBlock2(4); NSLog(@"%d", first); //  11 


In order to send messages to the object, the pointer to which we pass to the block, there is no need to mark the pointer as __block . After all, in fact, when we send a message to an object, we do not change its pointer.

Example of sending a message to an object from the context
 NSMutableArray *array = [NSMutableArray array]; void (^myBlock3)() = ^() { [array addObject:@"someString"];}; myBlock3(); //  someString  array 


But sometimes, nevertheless, it is necessary to mark a pointer to an object using __block to avoid memory leaks. (More on this in the “Memory Management” section)

C ++ lambda

Captured variables are specified in a specific place [5] , namely inside the square brackets []

The context specifier is related to the mutable specifier, it says
that you can change copies of variables passed by value. More in the next section.


Memory management


Obj-c blocks

Blocks are objects, they are created on the stack (afterwards they can be transferred to a heap)
Blocks can exist as 3 implementations [7] .

  1. When we do not use variables from the context (from the stack) inside the block, an NSGlobalBlock is created, which is implemented as a singleton.
  2. If we use context variables, then NSStackBlock is created, which is no longer a singleton but resides on the stack.
  3. If we use the Block_copy function, or we want our block to be saved inside some object located in the heap, for example, as a property of the object: @property (nonatomic, copy) MyBlockType myBlock; then an object of class NSMallocBlock is created that captures and captures (takes possession of == sends a retain message) objects passed in context. This is a very important property, because it can lead to memory leaks if you are not careful about it. Blocks can create cycles of ownership (retain cycle). It is also important to note that if we use the value of a property in NSMallocBlock , the property itself does not retain, but the object to which the property belongs.


Here is an example of the ownership cycle:
Suppose you want to create a class that makes an HTTP request with the asynchronous API PKHTTPReuquest

Implementing PKHTTPReuquest
 typedef void (^PKHTTPRequestCompletionSuccessBlock)(NSString *responseString); typedef void (^PKHTTPRequestCompletionFailBlock)(NSError* error); @protocol PKRequest <NSObject> - (void)startRequest; @end @interface PKHTTPRequest : NSObject <PKRequest> // designated initializer - (id)initWithURL:(NSURL *)url successBlock:(PKHTTPRequestCompletionSuccessBlock)success failBlock:(PKHTTPRequestCompletionFailBlock)fail; @end 

 @interface PKHTTPRequest () <NSURLConnectionDelegate> @property (nonatomic, copy) PKHTTPRequestCompletionSuccessBlock succesBlock; @property (nonatomic, copy) PKHTTPRequestCompletionFailBlock failBlock; @property (nonatomic, retain) NSURL *url; @property (nonatomic, retain) NSURLConnection *connection; @property (nonatomic, retain) NSMutableData *data; @end @implementation PKHTTPRequest #pragma mark - initialization / deallocation // designated initializer - (id)initWithURL:(NSURL *)url successBlock:(PKHTTPRequestCompletionSuccessBlock)success failBlock:(PKHTTPRequestCompletionFailBlock)fail { self = [super init]; if (self != nil) { self.succesBlock = success; self.failBlock = fail; self.url = url; NSURLRequest *request = [NSURLRequest requestWithURL:self.url]; self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO] autorelease]; } return self; } - (id)init { NSAssert(NO, @"Use desiganted initializer"); return nil; } - (void)dealloc { self.succesBlock = nil; self.failBlock = nil; self.url = nil; self.connection = nil; self.data = nil; [super dealloc]; } #pragma mark - public methods - (void)startRequest { self.data = [NSMutableData data]; [self.connection start]; } #pragma mark - NSURLConnectionDelegate implementation - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.data appendData:data]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { self.failBlock(error); self.data = nil; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { self.succesBlock([NSString stringWithUTF8String:self.data.bytes]); self.data = nil; } @end 



And then you want to create a specific request for a specific API of your server PKGetUserNameRequest that works with PKHTTPReuquest

Implementing PKGetUserNameRequest
 typedef void (^PKGetUserNameRequestCompletionSuccessBlock)(NSString *userName); typedef void (^PKGetUserNameRequestCompletionFailBlock)(NSError* error); @interface PKGetUserNameRequest : NSObject <PKRequest> - (id)initWithUserID:(NSString *)userID successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success failBlock:(PKGetUserNameRequestCompletionFailBlock)fail; @end 


 NSString *kApiHost = @"http://someApiHost.com"; NSString *kUserNameApiKey = @"username"; @interface PKGetUserNameRequest () @property (nonatomic, retain) PKHTTPRequest *httpRequest; - (NSString *)parseResponse:(NSString *)response; @end @implementation PKGetUserNameRequest #pragma mark - initialization / deallocation - (id)initWithUserID:(NSString *)userID successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success failBlock:(PKGetUserNameRequestCompletionFailBlock)fail { self = [super init]; if (self != nil) { NSString *requestString = [kApiHost stringByAppendingFormat:@"?%@=%@", kUserNameApiKey, userID]; self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString] successBlock:^(NSString *responseString) { //   -   self NSString *userName = [self parseResponse:responseString]; success(userName); } failBlock:^(NSError *error) { fail(error); } ] autorelease]; } return self; } - (id)init { NSAssert(NO, @"Use desiganted initializer"); return nil; } - (void)dealloc { self.httpRequest = nil; [super dealloc]; } #pragma mark - public methods - (void)startRequest { [self.httpRequest startRequest]; } #pragma mark - private methods - (NSString *)parseResponse:(NSString *)response { /* ...... */ return userName; } @end 



Error in this line NSString *userName = [self parseResponse:responseString]; - when we call something on self in the Malloc block, self retains, the following cycle is formed in the ownership graph:



This could be avoided by creating on the stack an intermediate pointer to self with the __block modifier, like this:

An example of a break in the ownership cycle
 //    __block PKGetUserNameRequest *selfRequest = self; self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString] successBlock:^(NSString *responseString) { NSString *userName = [selfRequest parseResponse:responseString]; success(userName); } failBlock:^(NSError *error) { fail(error); } ] autorelease]; 


Alternatively, it was possible to transfer blocks from the initialization method signature to the startRequest method,
startRequestwithCompaltion:fail: and re-run blocks only for the duration of the request. Then it would be possible to do without the __block modifier. This would solve another problem: in the example above, there is a danger that by the time the block is called (by the time the request is completed) the PKGetUserNameRequest object will no longer exist (the dealloc method will be called), since the block provides a weak link. And according to the selfRequest pointer, selfRequest will hang, which will cause a crash.

Let's give another example of incorrect memory management with blocks, an example is taken from the lecture video [7]

Error with NSStackBlock
 void addBlockToArray(NSMutableArray* array) { NSString *string = @"example string"; [array addObject:^{ printf("%@\n", string); }]; } void example() { NSMutableArray *array = [NSMutableArray array]; addBlockToArray(array); void (^block)() = [array objectAtIndex:0]; block(); } 


If we copied the block into a heap and passed it up the stack, there would be no error.
Also, this example will not cause errors in the ARC code.

C ++ lambda

Implementation lambd in runtime, can be specific in different compilers. They say that memory management for lambda is not very described in the standard. [9]
Consider the common implementation.
Lambda in C ++ are objects of unknown type created on the stack.
Lambdas that capture no context can be cast to a function pointer, but this does not mean that they themselves are simply function pointers. Lambda is a normal object, with a constructor and destructor, standing out on the stack.

Let's give some examples of moving lambda in heap
Example of moving lambda in heap
 //  â„–1 auto lamb = []() {return 5;}; auto func_lamb_ptr = new std::function<int()>(lamb); //  â„–2: auto lamb = []() {return 5;}; auto* p = new decltype(lamb)(lamb); //  â„–3: template <typename T> T* heap_alloc(T const& value) { return new T(value); } auto* p = heap_alloc([]() {return 5;}); //  â„–4: std::vector<decltype(lamb)> v; v.push_back(lamb); 


Now you can pass the function to a member variable of an object.

mutable after declaring lambda arguments says that you can change the values ​​of copies of variables captured by value (The value of the original variable will not change). For example, if we defined a lambda like this: auto lambda = [multiplier](int num) throw() mutable we could change the value of the multiplier inside the lambda, but the multipler declared in the function would not change. Moreover, the modified multiplier value is retained from the call to the call of this instance of lambda. You can think of it this way: in the lambda instance (in the object), variable members are created corresponding to the passed parameters. Here you need to be careful, because if we copy a copy of the lambda and call it, then these member variables will not change in the original lambda, they will change only in the copied one. Sometimes you need to pass the lambda wrapped in std::ref . Obj-C blocks do not provide such an opportunity out of the box.


Objective-C ++


Since Objecitve-C ++ combines both Objective-C and C ++, you can use lambda and blocks in it at the same time. How do lambda and blocks relate to each other?

  1. We can assign lambda to the block.
    Example
      void (^block_example)(int); auto lambda_example = [](int number){number++; NSLog(@"%d", number);}; block_example = lambda_example; block_example(10); // log 11 


  2. We can assign block std :: function

    Here it is worth noting that Objective-C and C ++ have different memory management policies, and storing a block in std::function can lead to hanging links.

  3. We can not assign a lambda block.

    The lambda has no copy-assignment operator defined. Therefore, we can not assign her a block or even herself.
    Assignment error
     int main() { auto lambda1 = []() -> void { printf("Lambda 1!\n"); }; lambda1 = lambda1; // error: use of deleted function 'main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&)' return 0; } 



It should be said that the operations between lambdas and blocks are quite exotic, for example, I have never met such assignments in projects.

Related Links


  1. About lambdas C ++
  2. Closures
  3. About Apple Blocks
  4. Comparison of lambda and blocks in English
  5. Docks C ++ lambda
  6. About blocks
  7. Great video about blocks
  8. Question about organizing C ++ lambda memory on stackoverflow.com
  9. Question about C ++ lambda implementation at runtime
  10. On the interaction of lambda and blocks
  11. Lambda syntax
  12. About Objective-C and C ++ Interaction
  13. Ways to embed C ++ in Objective-C projects

Source: https://habr.com/ru/post/204356/


All Articles