0

I have defined a set of macros to visually simplify my code when creating UIActionController and UIAlertAction statements. The macros work fine, but there is one situation that causes compiler errors. Although there is a simple workaround, I'm trying to understand why these errors occur and how to rewrite the macro to eliminate the need for the workaround.

There are two macros defined, below. The first macro is included only to allow the example code to compile.

My issue concerns the second macro. It has two arguments. One is a string and the other is an actionBlock (for a completionHandler). If the actionBlock contains a statement with an array or a method call with multiple arguments (that resemble an array), the compiler complains. The errors can be eliminated by adding a pair of parentheses, as shown.

- (void)macroTest
{
#define ALERT_CREATE( title, msg )  \
    UIAlertController *alertController = \
        [UIAlertController alertControllerWithTitle:title \
        message:msg preferredStyle:UIAlertControllerStyleAlert]

#define ALERT_DEFAULT_ACTION( title, actionBlock ) \
    [alertController addAction:[UIAlertAction actionWithTitle:title \
        style:UIAlertActionStyleDefault \
        handler:^(UIAlertAction *action) { actionBlock } ]]

    // Example code
    ALERT_CREATE( @"Alert Title", @"Alert Message" );

    // This example is fine (no errors):
    ALERT_DEFAULT_ACTION( @"Action 1 Title" , /* actionBlock */
                         int i = 1;
                         i++;
                         );

    // This results in error: Use of undeclared identifier 'ALERT_DEFAULT_ACTION'
    ALERT_DEFAULT_ACTION( @"Action 2a Title" ,
                         // Another error: Too many arguments provided to function-like macro invocation
                         NSArray *test = @[ @"1", @"2" ]; // not okay
                         );

    ALERT_DEFAULT_ACTION( @"Action 2b Title" ,
                         // The same code as 2a, but with parens, is okay
                         NSArray *test = (@[ @"1", @"2" ]); // this is okay (with parens)
                         );

    ALERT_DEFAULT_ACTION( @"Action 3 Title" ,
                         // Parentheses are needed here, too, to avoid error
                         NSString *str1 = [NSString stringWithFormat:(@"format %@", @"arg")];                                   NSString *str2 = ([NSString stringWithFormat:@"format %@", @"arg"]);
                         );

    `enter code here`// The next line displays the alert.  It's commented out to avoid errors in this example.
    // [self presentViewController:alertController animated:YES completion:nil];
}

Why does the compiler complain and how can I rewrite the second macro so I don't have to add the parentheses to some of the statements in its actionBlock? I'm using Xcode 11.3.

UPDATE: It also confuses Xcode's indentation logic if there's a conditional in the actionBlock. To wit:

ALERT_DEFAULT_ACTION( @"Action 4a Title" ,
                     // code here, correct indentation
                     );
ALERT_DEFAULT_ACTION( @"Action 4b Title" ,
                     if ( true ) {
    // code here, incorrect indentation
} // incorrect indenation
                     );
Jeff
  • 2,659
  • 1
  • 22
  • 41

1 Answers1

0

My question should have been marked as a duplicate because there are several similar questions with answers. The trick is using good search terms (hint, search for: passing a block in a macro).

The solution is to use variadic functions ("..." and VA_ARGS). The last argument to the macro gobbles up everything in the block, including commas, as explained in this excellent article: Sending blocks as arguments to macros https://mort.coffee/home/obscure-c-features/

In case that link becomes orphaned, here's the important point:

The pre-processor supports variadic macros; macros which can take an unlimited amount of arguments. The way they are implemented is that any extra arguments (indicated by ... in the macro definition) are made available through the VA_ARGS identifier; VA_ARGS is replaced with all the extra arguments separated by commas. [This passes] a block of code, with unguarded commas, to a macro.

That post gives credit to this early solution from 2012: https://stackoverflow.com/a/13842612/1578823 So, +1 there.

Here's my fixed macro:

#define ALERT_DEFAULT_ACTION( title, ... /* actionBlock */ ) \
    [alertController addAction:[UIAlertAction actionWithTitle:title \
        style:UIAlertActionStyleDefault \
        handler:^(UIAlertAction *action) { {__VA_ARGS__} /* actionBlock */ }]]

This does not solve the issue of incorrectly indented brackets (e.g., in if-statements). That's just too confusing for Xcode.

ALERT_DEFAULT_ACTION( @"title",
                     NSArray *test = @[ @"1", @"2" ]; // commas okay now
                     if ( true ) {
    test = test; // incorrect indentation
} // incorrect indentation
                     test = test;
                     );
Jeff
  • 2,659
  • 1
  • 22
  • 41