Play with iOS "macro definition"

The definition of macro is very important in class C language, because macro is a function of precompiling, so it can control program flow at a higher level than runtime. When you first learn the definition of macro, you may have the feeling that it's a complete replacement. It's too simple. But if you really think so, you will be naive, not to mention writing your own macros, many of the macros defined in the Foundation framework will take a lot of brains to understand. This blog summarizes the experience of predecessors, and collects some very clever macros for analysis, hoping to help you have a deeper understanding of the definition of macros, and can apply your experience to the actual development.

1, Preparation

The essence of macro is the replacement of precompiled text. Before we start the text, we need to introduce a method to observe the result of macro replacement, which helps us to verify and test the final result of macro more conveniently. Xcode development tool has the function of viewing precompiled results. First, you need to compile the project, and then select the Assistant option in the toolbar to open the Assistant window, as shown in the following figure:

Then select the Preprocess option of the window to open the precompiled result window. You can see the final result after the macro is replaced, as shown in the following figure:

Later, we will use this method to verify the macro.

2, About macro definition

Macro is defined by ා define. There are two kinds of macro definitions: object macro and function macro. Object macros are usually used to define quantity values. When precompiling, the macro name is directly replaced by the corresponding quantity value. When defining function macros, parameters can be set, and their functions are very similar to those of functions.

For example, we can define the value of π as an object macro. When using it, it is much more convenient to use meaningful macro names than to directly use π's literal value. For example:

#import <Foundation/Foundation.h>
#define PI 3.1415926
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = PI * 3;
        NSLog(@"%f", res);
    }
    return 0;
}

Functional macros should be more flexible. For example, for the method of circle area calculation, we can define it as a macro:

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1);
        NSLog(@"%f", res);
    }
    return 0;
}

Now, with this area calculation macro, we can calculate the area of the circle more conveniently, which looks perfect. Later, we will use this functional macro as an example to understand the principle of macro in depth.

3, Starting from a simple functional macro

If you look at the macro we wrote to calculate the area above, normally it seems that there is no problem. However, it should be noted that the macro is not a function in the final analysis. If you use it as a function completely, we may fall into a series of traps, such as:

#define PI 3.1415926
#define CircleArea(r) PI * r * r
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        CGFloat res = CircleArea(1 + 1);
        NSLog(@"%f", res);
    }
    return 0;
}

When running the code, the result of the operation is not the area with a radius of 2 circles. What's wrong? Let's look at the result of the macro precompiled first:

CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;

At a glance, because of the priority problem of operators, the operation order is wrong. In programming, all the problems caused by operator priority can be solved in one way: using parentheses. The circle area macro is modified as follows:

#define CircleArea(r) (PI * (r) * (r))

The execution order is controlled by force, and the code execution returns to normal. It seems that there is no problem. It's too early to be satisfied. For example, use this macro as follows:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r) PI * (r) * (r)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

During operation, it is found that the result is wrong again. Not only is the calculation result inconsistent with our expectation, but the result of variable self addition is also wrong. We check the expanded result:

CGFloat res = 3.1415926 * (r++) * (r++);

The original problem is that when the macro is expanded, the parameter is replaced twice. Since the parameter itself is a self adding expression, it is added twice by itself, which causes a problem. How to solve this problem? There is a very useful syntax in C language, that is, use braces to define the code block, and the code block will return the execution result of the last statement, modify the above The macro is defined as follows:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int r = 1;
        CGFloat res = CircleArea(r++);
        NSLog(@"%f, %d", res, r);
    }
    return 0;
}

This time the program is back to normal. However, if the name of the variable in the call macro is the same as the temporary variable in the macro, the disaster will happen again, for example:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define CircleArea(r)   \
({                      \
    typeof(r) _r = r;   \
    (PI * (_r) * (_r)); \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int _r = 1;
        CGFloat res = CircleArea(_r);
        NSLog(@"%f, %d", res, _r);
    }
    return 0;
}

Running the above code, you will find that the temporary variables in the macro are not initialized successfully. This is really hard. We are going further. For example, we are doing some tricks on the names of temporary variables and naming them as names that are not easy to repeat. In fact, a macro built in the system is specially used to construct unique variable names: "count" is a COUNTER, which will be accumulated automatically during compilation, and the macro we wrote will be modified again, as follows:

#import <Foundation/Foundation.h>
#define PI 3.1415926
#define PAST(A, B) A##B
#define CircleArea(r)   __CircleArea(r, __COUNTER__)
#define __CircleArea(r, v)      \
({                              \
    typeof(r) PAST(_r, v) = r;         \
    (PI * PAST(_r, v) * PAST(_r, v));     \
})
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = CircleArea(_r);
        CGFloat res2 = CircleArea(_r);
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

After the modification here, our macro is not so easy to understand. First of all, the COUNTER will be auto incremented every time the macro is replaced. The COUNTER is a special symbol used to splice parameters together. However, it should be noted that if the macro spliced by the COUNTER is another macro, it will prevent the expansion of the macro. Therefore, we define a conversion macro pass (a, B) To handle splices. If you can't understand why this can solve the problem of macro expansion at once, you just need to remember the principle of macro expansion: if parameters use the ා or ා, the expansion of macro parameters will not be carried out, otherwise, the macro parameters will be expanded first, and the macro will be expanded before the expansion. The final precompiled result of the above code is as follows:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int _r = 1;
        CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });
        CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });
        NSLog(@"%f, %f", res, res2);
    }
    return 0;
}

A simple macro to calculate the area of a circle. For safety, we have done so many things. It seems that it is not easy to use a macro well.

4, Good habits when writing macros

Through the previous introduction, we know that it's irresponsible to write a macro at will. It seems that it's totally different from using it in any scenario. When writing macros, we can deliberately cultivate the following coding habits:

  • Parentheses shall be added to parameters and calculation results

This principle should not need to be discussed. As shown in the previous example, adding parentheses completely can avoid many exception problems caused by operator priority.

  • Multi statement functional macro, to use do while package

This principle seems strange, but it is very important. For example, we need to write a custom LOG macro and add some custom information when printing. You may write as follows:

#define LOG(string)     \
NSLog(@"Customized information");   \
NSLog(string);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LOG(@"info")
    }
    return 0;
}

There seems to be no problem running the code at present, but if it is combined with the if statement, there may be problems:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"info")
    }
    return 0;
}

After running the code, there is still a line of LOG information output. See the precompiled results as follows:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            NSLog(@"Customized information"); NSLog(@"info");
    }
    return 0;
}

The problem is found. If the if structure is not standardized with braces, its default scope is only one code, so it is not a problem to write more braces. Therefore, it is a good habit to add braces when writing multi statement macros, as follows:

#define LOG(string)     \
{NSLog(@"Customized information");   \
NSLog(string);}

This solves the problem, but it's not perfect. Let's say this when using:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO");
        else
            LOG(@"YES");
    }
    return 0;
}

It is found that errors are still reported due to semicolon tampering. The precompiled results are as follows:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            {NSLog(@"Customized information"); NSLog(@"NO");};
        else
            {NSLog(@"Customized information"); NSLog(@"YES");};
    }
    return 0;
}

We know that there is no need for semicolons after the braces of syntax structure blocks such as if, while, for. In order to be compatible with single line if statements, we forced a brace to expand them into multiple lines due to the reason of macros. A good way to solve this problem is to really convert multi line macros into single statements. The do whlie structure can achieve this effect Therefore, modify the macro as follows:

#define LOG(string)     \
do {NSLog(@"Customized information");   \
NSLog(string);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"NO")
        else
            LOG(@"YES");
    }
    return 0;
}

After precompiled translation:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (__objc_no)
            do {NSLog(@"Customized information"); NSLog(@"NO");} while(0);
        else
            do {NSLog(@"Customized information"); NSLog(@"YES");} while(0);;
    }
    return 0;
}

Now, no matter how it is used outside, this Honghong can work normally.

  • For a macro with indefinite parameters, the parameters are spliced with the help of ××××× symbol

When defining a function, we can define the parameter of the function as an indefinite number parameter. Similarly, when defining a functional macro, we can use the symbol "..." to specify an indefinite number parameter, such as adjusting a LOG macro, as follows:

#define LOG(format, ...)     \
do {NSLog(@"Customized information");   \
NSLog(format, __VA_ARGS__);} while(0);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d", NO)
        else
            LOG(@"%d", YES);
    }
    return 0;
}

__VA ﹣ args ﹣ is also a built-in macro symbol. Its function is to represent the variable parameter "..." in the macro definition. It should be noted that if we pass in 0 variable parameters according to the above writing method, there will be a problem. The reason is that there is an extra comma, for example:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        if (NO)
            LOG(@"%d") // This will be precompiled to nslog (@% D,,)
        else
            LOG(@"%d", YES);
    }
    return 0;
}

The solution is to splice the variable parameters once. When using the ××××× symbol to splice the parameters, if the latter parameter is empty, the comma in front will be removed automatically, as follows:

#define LOG(format, ...)     \
do {NSLog(@"Customized information");   \
NSLog(format, ##__VA_ARGS__);} while(0);

5, Special macro symbols and common built-in macros

There are several special symbols that can make macro definition very flexible. The commonly used special symbols and special macros are listed as follows:

  • #

The well number is used to string parameters, such as:

#define Test(p) #p

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(abc); // It becomes "abc" after precompiled translation;
    }
    return 0;
}
  • ##

We have used the double well number before, its function is to splice parameters, for example:

#define Test(a,b) a##b

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Test(1,2); // 12 after precompiled translation;
    }
    return 0;
}
  • __VA_ATGS__

Special in the variable parameter macro, indicating all the passed variable parameters.

  • __COUNTER__

A cumulative count macro, often used to construct unique variable names.

  • __LINE__

A commonly used built-in macro when recording LOG information. It will be replaced with the current line number when precompiling.

  • __FILE__

When recording LOG information, a commonly used built-in macro will be replaced with the full path of the current file during precompiling.

  • __FILE_NAME__

A commonly used built-in macro when recording LOG information. It will be replaced with the current file name when precompiling.

  • __DATE__

When recording LOG information, a commonly used built-in macro will be replaced with the current date when precompiling.

  • __TIME__

When recording LOG information, a commonly used built-in macro will be replaced with the current time during precompiling.

6, Macro expansion rules

Through the previous introduction, we have no big problems with the application of macros, and we have also learned a lot of tips for using macros. In this section, we will discuss the replacement rules of macros in more depth. Macros themselves support nesting, for example:

#define M1(A) M2(A)
#define M2(A) A
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1);
    }
    return 0;
}

The two macros defined in the above code are basically meaningless. The result of replacing M1 macro is M2 macro. M2 macro is finally replaced by parameter itself. From this example, we can see that macro can be nested recursively expanded, but recursively expanded is a principle, and infinite recursion will not occur, for example:

#define M1(A) M2(A)
#define M2(A) M1(A)
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        M1(1); // The final expansion is M1(1)
    }
    return 0;
}

The expansion of a macro should conform to the following principles:

  1. In the process of macro expansion, parameters will be expanded first. If parameters are spliced or processed with ා, this parameter will not be expanded.
  2. During macro expansion, if a macro to be expanded appears in the replacement list, the macro will not be expanded.

The above expansion principle refers to the replacement list. In the process of macro expansion, a replacement list will be maintained. In the process of expansion, it needs to replace the macro layer by layer from the parameter to the macro itself, from the outer macro to the inner macro. Each time the macro name is replaced, it will be put into the maintenance replacement list. In the next round of replacement, if the macro name in the replacement list appears again , will not be replaced again. Take the code above as an example to analyze:

  1. First, M1 macro is replaced by M2 after the first round of replacement, and then the macro name M1 is placed in the replacement list.
  2. M2 is still a macro name. In the second round, replace M2 with M1, and put m2 in the replacement list again. At this time, there are macro names M1 and M2 in the replacement list.
  3. M1 is still the macro name, but M1 already exists in the replacement list. This macro name will not be expanded.

7, The magic of macro

In this section, we will turn to be connoisseurs to analyze and appreciate many practical and ingenious cases of macro. From these excellent use cases, we can broaden our thinking of using macros.

  1. MIN and MAX

Foundataion has built in some commonly used operation macros, such as getting the maximum, minimum, absolute value of two numbers, etc. Taking MAX macro as an example, the compilation of this macro basically covers all the points to be paid attention to in functional macro, as follows:

#define __NSX_PASTE__(A,B) A##B
#if !defined(MAX)
    #define __NSMAX_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__b,L) : __NSX_PASTE__(__a,L); })
    #define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)
#endif

Among them, the nsmax impl macro constructs the unique internal variable name with the help of the count COUNTER and the concatenate NSX paste macro. The writing method of the example macro we provided earlier also basically refers to this system macro. Later, when you write a functional macro, you can refer to the implementation of this macro.

2. NSAssert, etc

NSAssert is an assertion macro, which is often used for security in development and debugging. The definition of this macro is as follows:

#define NSAssert(condition, desc, ...)	\
    do {				\
	__PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
	if (__builtin_expect(!(condition), 0)) {		\
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
	    [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
		object:self file:__assert_file__ \
	    	lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
	}				\
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)

NSAssert macro definition uses the technique of splicing indefinite parameters to eliminate commas, and it is a practice of multi line macro statements using do while to optimize.

3. @ weakify and @ strongify

weakify and strongify are two macros commonly used in react cocoa to deal with circular references. The definitions of these two macros are very ingenious. For example, to understand this macro, it is not very simple. First, the macro definitions related to this macro are listed as follows:

#if DEBUG
#define rac_keywordify autoreleasepool {}
#else
#define rac_keywordify try {} @catch (...) {}
#endif

#define rac_weakify_(INDEX, CONTEXT, VAR) \
CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);

#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__)

#define metamacro_argcount(...) \
metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)
#define metamacro_at(N, ...) \
metamacro_concat(metamacro_at, N)(__VA_ARGS__)

#define metamacro_concat(A, B) \
metamacro_concat_(A, B)

#define metamacro_concat_(A, B) A ## B

#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)

#define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0)

#define metamacro_head_(FIRST, ...) FIRST

RAC ﹣ keyword dify distinguishes between DEBUG and RELEASE environments. In the DEBUG environment, it actually creates a useless autorelease pool to eliminate the previous @Symbols In the RELEASE environment, it creates a try catch structure to eliminate parameter warnings. The metamacro > foreach > CXT macro is complex, and its expansion process is as follows:

// Step 1: original macro
metamacro_foreach_cxt(rac_weakify_,, __weak, obj)
// Step 2: expand metamacro? Foreach? CXT
metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(obj))(rac_weakify_,, __weak, obj)
// Step 3: expand metamacro argcount      
metamacro_concat(metamacro_foreach_cxt, metamacro_at(20, obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// Step 4: expand metamacro at       
metamacro_concat(metamacro_foreach_cxt,metamacro_concat(metamacro_at, 20)(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// Step 5: expand metamacro ABCD concat       
metamacro_concat(metamacro_foreach_cxt,metamacro_at20(obj, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1))(rac_weakify_,, __weak, obj)
// Step 6: expand metamacro_at20        
metamacro_concat(metamacro_foreach_cxt,metamacro_head(1))(rac_weakify_,, __weak, obj)
// Step 7: expand metamacro head        
metamacro_concat(metamacro_foreach_cxt,metamacro_head_(1, 0))(rac_weakify_,, __weak, obj)
// Step 8: expand metamacro head_      
metamacro_concat(metamacro_foreach_cxt,1)(rac_weakify_,, __weak, obj)
// Step 9: expand metamacro ﹣ concat        
metamacro_foreach_cxt1(rac_weakify_,, __weak, obj)
// Step 10: expand metamacro ABCD foreach ABCD cxt1
rac_weakify_(0, __weak, obj)
// Step 11: expand RAC  weakify_
__weak __typeof__(obj) metamacro_concat(obj, _weak_) = (obj);
// Step 12: expand metamacro ﹣ concat        
__weak __typeof__(obj) obj_weak_ = (obj);

The expansion of the strongify macro is similar.

4. ParagraphStyleSet macro

ParagraphStyleSet macro is a shortcut provided by YYLabel to set properties related to ParagraphStyle. One of the techniques used is to directly use the parameter of macro as the property name, so that the settings of various properties can be completed by using the same macro. Its definition is as follows:

#define ParagraphStyleSet(_attr_) \
[self enumerateAttribute:NSParagraphStyleAttributeName \
                 inRange:range \
                 options:kNilOptions \
              usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \
                  NSMutableParagraphStyle *style = nil; \
                  if (value) { \
                      if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \
                          value = [NSParagraphStyle yy_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \
                      } \
                      if (value. _attr_ == _attr_) return; \
                      if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \
                          style = (id)value; \
                      } else { \
                          style = value.mutableCopy; \
                      } \
                  } else { \
                      if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \
                      style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \
                  } \
                  style. _attr_ = _attr_; \
                  [self yy_setParagraphStyle:style range:subRange]; \
              }];

8, Conclusion

Macro seems simple, but it's not easy to use it well. I think the best way to learn is to use it constantly in practical application, and constantly ponder and optimize it. If you can use macro easily, it will definitely improve your code ability.

Focus on technology, love life, exchange technology, also be friends.

——Hunshao QQ group: 805263726

Keywords: Front-end C xcode Programming React

Added by Consolas on Wed, 22 Apr 2020 12:57:31 +0300