Objective-c blocks and c ++ lambdas

  • Tutorial
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, mechanisms for capturing the context, memory management and the interaction of lambdas and blocks with each other.
All examples used Apple LLVM Compiler 4.2 (Clang). ARC is not used for the Obj-C code, because I am of the opinion that you need 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 are an implementation of closures [2] . Blocks are anonymous functions that can capture the context (current stack variables and class member variables). Blocks at runtime are represented by objects; they are analogues of lambda expressions in C ++.

Lambdas in C ++ are also implementation of closures and represent anonymous 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 accepts one parameter of type int, and returns a parameter of type int
  • multiplier - a variable that comes to us from the context (more about this in the section “Capturing the context”)


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

Block Usage Example 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, you can verify this 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 ++ lambdas

The same lambda code
[11]
In text form
int multiplier = 7;
auto lambda = [&multiplier](int num) throw() -> int
{
     return multiplier * num;
};
lambda(4); // равно 28

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


We give a similar example of selecting a subset for a lambda

Retrieve a subset from a predicate collection
template
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 inputVector = {5, 3, 8, 9, 2};
        std::vector 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 above example, we did not indicate in the block declaration that we want to capture a variable multiplier(unlike a lambda, in a 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 body of the function. If we wanted to change its value in the body of the block, we would have to mark the variable with a modifier__block

Example of changing a variable value from 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 __blocknot. Indeed, in fact, when we send a message to an object, we do not change its pointer.

An example of sending a message to an object from context
NSMutableArray *array = [NSMutableArray array];
void (^myBlock3)() = ^() { [array addObject:@"someString"];};
myBlock3(); // добавит someString в array


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

C ++ lambdas

The captured variables are indicated in a specific place [5] , namely inside the square brackets []
  • [&] - means that we capture all the characters on the link
  • [=] - all characters by value
  • [a, &b]- acaptured by value, bcaptured by reference
  • [] - nothing is captured

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


Memory management


Obj-C blocks

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

  1. When we do not use variables from the context (from the stack) inside the block, it is created NSGlobalBlock, which is implemented as a singleton.
  2. If we use context variables, then it is created NSStackBlock, which is no longer a singleton, but is located on the stack.
  3. If we use a function Block_copy, or we want our block to be stored inside some object placed in a heap, for example, as a property of an object: @property (nonatomic, copy) MyBlockType myBlock;then an object of a class is created NSMallocBlockthat captures and masters (masters == sends a message retain) objects transferred in context. This is a very important property, because it can lead to memory leaks if it is not handled carefully. Blocks can create retain cycles. It is also important to note that if we use the value propertyin NSMallocBlock- it will not be the property itself that will retain, but the object to which the property belongs.


Here is an example ownership cycle:
Let's say you wanted to create a class that makes a HTTPrequest with asynchronousAPI PKHTTPReuquest

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

@interface PKHTTPRequest () 
@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 wanted to create a specific request for a specific API of your server PKGetUserNameRequestthat works withPKHTTPReuquest

PKGetUserNameRequest implementation
typedef void (^PKGetUserNameRequestCompletionSuccessBlock)(NSString *userName);
typedef void (^PKGetUserNameRequestCompletionFailBlock)(NSError* error);
@interface PKGetUserNameRequest : NSObject 
- (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



The error in this line NSString *userName = [self parseResponse:responseString];is that when we call something in self in the Malloc block, self resets, the following loop forms in the ownership graph:



You could avoid this by creating an intermediate pointer to self on the stack with the __block modifier, like this:

Example of breaking a tenure 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, you can transfer blocks of method signature initialization method startRequest,
startRequestwithCompaltion:fail:and reteynit blocks only for the duration of the request. Then one could do without a modifier __block. This would solve another problem: in the above example, there is a danger that by the time the block is called (by the time the request is completed), an object of type PKGetUserNameRequest will cease to exist (the dealloc method will be called), since the block provides weak communication. And selfRequestzombies will hang on the pointer , which will cause crash.

We give another example of incorrect memory management with blocks, an example taken from the video lecture [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 up the stack, an error would not have occurred.
Also, this example will not cause errors in the ARC code.

C ++ lambdas

The implementation of lambdas in runtime may be specific in different compilers. They say that memory management for lambdas is not very described in the standard. [9]
Consider a common implementation.
Lambdas in C ++ are objects of an unknown type that are created on the stack.
Lambdas that do not capture any context can be cast to a function pointer, but still this does not mean that they themselves are just pointers to a function. A lambda is an ordinary object, with a constructor and a destructor, standing out on the stack.

Here are some examples of moving lambdas in heap
An example of moving a lambda in heap
// Способ №1
auto lamb = []() {return 5;};
auto func_lamb_ptr = new std::function(lamb);
// Способ №2:
auto lamb = []() {return 5;};
auto* p = new decltype(lamb)(lamb);
// Способ №3:
template 
T* heap_alloc(T const& value)
{
    return new T(value);
}
auto* p = heap_alloc([]() {return 5;});
// Способ №4:
std::vector v;
v.push_back(lamb);


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

mutable after declaring lambda arguments, it means 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() mutablewe could change the value multiplierinside the lambda, but the multipler declared in the function has not changed. Moreover, the changed valuemultipliersaved from call to call of this lambda instance. You can imagine it this way: in a lambda instance (in an object) variable members are created corresponding to the passed parameters. We need to be careful here, because if we copy an instance of a lambda and call it, then these member variables will not change in the original lambda, they will only change in the copied one. Sometimes you need to pass lambdas 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 lambdas and blocks at the same time. How do lambdas and blocks relate to each other?

  1. We can assign a 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 the block to the std :: function object.

    Here it is worth noting that Objective-C and C ++ have different memory management policies, and storing the block in std::functioncan lead to “hanging” links.

  3. We cannot assign a lambda block.

    Lambda does not have a copy-assignment statement defined. Therefore, we cannot assign to it either a block or even ourselves.
    Assignment Error
    int main() {
        auto lambda1 = []() -> void { printf("Lambda 1!\n"); };
        lambda1 = lambda1;  // error: use of deleted function ‘main()::& main()::::operator=(const main()::&)’
        return 0;
    }
    



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

Related Links


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

Also popular now: