A weapon to improve code compulsion: macro definition-from entry to abandonment


Brother Dao's original 019

I. Introduction

I have always had this feeling: when I learn a new field of knowledge, if a certain point of knowledge is difficult to understand when I first come into contact with it , then no matter how much I spend in the future If I study this knowledge point for a long time, I will always think that it is more difficult, which means that the first impression is particularly important.

For example, the macro definition in the C language seems to be the same as mine. I always think that the macro definition is the hardest part of the C language , just like some friends always think that the pointer is the hardest part of the C language.

The essence of the macro is the code generator, which realizes the dynamic generation of the code with the support of the preprocessor, and the specific operation is realized through conditional compilation and macro expansion. We first establish such a basic concept in our mind, and then through the actual description and code to in-depth experience: how to control the macro definition.

So, today we are here to summarize and dig deep into all the knowledge points of macro definitions. I hope that after this article, I can get rid of this psychological barrier. After reading this summary article, I believe you will also be able to have an overall and overall grasp of macro definitions.

Two, the operation of the preprocessor

1. The effective link of the macro: preprocessing

When a C program is compiled, a binary executable file is generated from the beginning of the source file to the end , and it goes through 4 stages:

What we are discussing today is in the first session: preprocessing by the preprocessor to complete this phase of work, which includes the following four tasks:

  1. File introduction (#include);
  2. Conditional compilation (#if…#elif…#endif);
  3. Macro expansions;
  4. Line control.

2. Conditional compilation

Under normal circumstances, every line of code in the C language file needs to be compiled, but sometimes due to the optimization of the program code, you want to compile only part of the code , and you need to add it to the program at this time Condition , let the compiler compile only the code that meets the condition, and discard the code that does not meet the condition, this is conditional compilation .

To put it simply: the preprocessor dynamically processes the code according to the conditions we set, outputs the valid code to an intermediate file, and then sends it to the compiler for compilation.

Conditional compilation is basically used in all project codes. For example, when you need to consider the following situations, you will definitely use conditional compilation :

  1. The program needs to be compiled into executable programs under different platforms;
  2. The same set of code needs to run on different functional products on the same platform;
  3. There are some code for testing purposes in the program. If you don't want to pollute the product-level code, you need to block it.

Here are three examples of conditional compilation that are often seen in code:

Example 1: Used to distinguish C and C++ code

#ifdef __cplusplus 
extern "C" { 
#endif 
 
void hello();
 
#ifdef __cplusplus 
} 
#endif 

Such code can be seen in almost every open source library. The main purpose is to mix C and C++ programming, specifically:

  1. If you use gcc to compile, then the macro __cplusplus will not exist, and the extern “C” will be ignored;
  2. If you use g++ to compile, then the macro __cplusplus will exist, and the extern “C” will take effect. The compiled function name hello will not be rewritten by the g++ compiler, so it can be called by C code;

Example 2: Used to distinguish different platforms

#if defined(linux) || defined(__linux) || defined(__linux__)
    sleep(1000 * 1000); // 调用 Linux 平台下的库函数
#elif defined(WIN32) || defined(_WIN32)
    Sleep(1000 * 1000); // 调用 Windows 平台下的库函数(第一个字母是大写)
#endif

So, these linux, __linux, __linux__, WIN32, _WIN32are come from? We can think of it as the compilation target platform (operating system) prepared for us in advance.

Example 3: When writing a dynamic library under the Windows platform, declare export and import functions

#if defined(linux) || defined(__linux) || defined(__linux__)
    #define LIBA_API 
#else
	#ifdef LIBA_STATIC
		#define LIBA_API
	#else
	    #ifdef LIBA_API_EXPORTS
	        #define LIBA_API __declspec(dllexport)
	    #else
	        #define LIBA_API __declspec(dllimport)
	    #endif
	#endif
#endif

LIBA_API void hello();

This code was taken directly from the example in a small video I recorded at station B. At that time, it was mainly to demonstrate how to use make and cmake build tools to compile under the Linux platform . Later, my partner asked me to run it on Windows. The platform also uses make and cmake to build , so the above macro definition is written.

  1. When using MSVC to compile dynamic libraries, you need to define the macro LIBA_API_EXPORTS in the compilation options (Makefle or CMakeLists.txt), then the first macro LIBA_API of the exported function hello will be replaced with: __declspec(dllexport), which means the export operation;
  2. When compiling the application, using the dynamic library, you need to include the header file of the dynamic library. At this time, there is no need to define the macro LIBA_API_EXPORTS in the compilation options, then the LIBA_API at the front of the hello function will be replaced with __declspec(dllimport), which means Import operation
  3. One additional point: If you use a static library and no macro definitions are required in the compilation options, then the macro LIBA_API will be empty.

3. Platform predefined macros

As we have seen above, the target platform will predefine some macros for us to facilitate our use in the program. In addition to the above operating system-related macros, there is another type of macro definition, which is widely used in the logging system:

FILE : current source code file name;
LINE : current source code line number;
FUNCTION : currently executed function name;
DATE : compilation date;
TIME : compilation time;

E.g:

printf("file name: %s, function name = %s, current line:%d \n", __FILE__, __FUNCTION__, __LINE__);

Three, macro expansion

The so-called macro expansion is code replacement , and this part is also the main content I want to express. The biggest benefits of macro expansion are as follows:

  1. Reduce repetitive code;
  2. Complete some functions that cannot be achieved through C syntax (string splicing);
  3. Dynamically define data types to achieve functions similar to templates in C++;
  4. The program is easier to understand and modify (for example: numbers and strings are always on);

When we write the code, all the places where the macro name is used can be understood as a placeholder . In the preprocessing step of the compiler, these macro names will be replaced with those code segments in the macro definition. Note: it is only a simple text replacement .

1. The most common macros

In order to facilitate the following description, let's first look at a few common macro definitions:

(1) Definition of data type

#ifndef BOOL
    typedef char BOOL;
#endif

#ifndef TRUE
    #define TRUE
#endif

#ifndef FALSE
    #define FALSE
#endif

In the data type definition, one thing to note is: if your program needs to be compiled with a compiler under a different platform, then you have to check whether the data type controlled by these macro definitions has been defined by the compiler you are using . For example: there is no BOOL type in gcc, but in MSVC, the BOOL type is defined as an int type.

(2) Get the maximum and minimum values

#define MAX(a, b)    (((a) > (b)) ? (a) : (b))
#define MIN(a, b)    (((a) < (b)) ? (a) : (b))

(3) Count the number of elements in the array

#define ARRAY_SIZE(x)    (sizeof(x) / sizeof((x)[0]))

(4) Bit operation

#define BIT_MASK(x)         (1 << (x))
#define BIT_GET(x, y)       (((x) >> (y)) & 0x01u)
#define BIT_SET(x, y)       ((x) | (1 << (y)))
#define BIT_CLR(x, y)       ((x) & (~(1 << (y))))
#define BIT_INVERT(x, y)    ((x) ^ (1 << (y)))

2. Differences from functions

Judging from the above macros, all these operations can be implemented through functions , so what are their advantages and disadvantages?

Through the function to achieve:

  1. The type of formal parameters needs to be determined, and the parameters are checked when calling;
  2. Additional overhead is required when calling functions: operating formal parameters, return values, etc. in the function stack;

Realize through macros:

  1. No need to check parameters, more flexible parameter transfer;
  2. Directly expand the code of the macro, no function call is required during execution;
  3. If the same macro is called in multiple places, it will increase the code size;

It’s better to give an example, let’s take the comparison above:

(1) Use macros to achieve

#define MAX(a, b)    (((a) > (b)) ? (a) : (b))

int main()
{
    printf("max: %d \n", MAX(1, 2));
}

(2) Use functions to achieve

int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

int main()
{
    printf("max: %d \n", max(1, 2));
}

Except for the overhead of the function call, there seems to be no difference. Here we compare 2 integer data , so what if we need to compare 2 floating-point data ?

  1. Use a macro to call: MAX (1.1, 2.2); everything is OK;
  2. Use function call: max(1.1, 2.2); Compile error: Type does not match.

At this time, the advantage of using macros to achieve is reflected: because there is no concept of types in macros, the caller can pass in any data type , and then in the subsequent comparison operations, the greater than or less than operations all use the C language The grammar itself is executed.

If you use a function to implement it, you must define another function to manipulate the floating point type, and you may compare it later: char type, long type data, and so on.

In C++ , such operations can be implemented through parameter templates . The so-called templates are also a dynamic code generation mechanism. When a function template is defined, multiple functions are dynamically generated according to the actual parameters of the caller . For example, define the following function template:

template<typename T> T max(T a, T b){
    if (a > b) 
        return a;
    return b;
}

max(1, 2);     // 实参是整型
max(1.1, 2,2); // 实参是浮点型

When the compiler sees max(1, 2), it will dynamically generate a function int max(int a, int b) { ... };

When the compiler sees max(1.1, 2.2)time, it will dynamically generate another function float max(float a, float b) { ... }.

Therefore, from the perspective of dynamic code generation, macro definitions are somewhat similar to template parameters in C++, except that macro definitions are just code extensions.

The following example is also quite good, using the macro type irrespective of the type to dynamically generate the structure :

#define VEC(T)          \
    struct vector_##T { \
        T *data;       \
        size_t size;    \
    };

int main()
{
    VEC(int)   vec_1 = { .data = NULL, .size = 0 };
    VEC(float) vec_2 = { .data = NULL, .size = 0 };
}

## is used in this example, this knowledge point will be explained below. In the previous example, the macro parameters passed are some variables , and the macro parameters passed here are data types . Through the type independence of the macro, the purpose of "dynamically" creating a structure is achieved:

struct vector_int {
    int *data;
    size_t size;
}

struct vector_float {
    float *data;
    size_t size;
}

There is a trap to note here : there can be no spaces in the passed data type , if you use it like this:, VEC(long long)then you will get:

struct vector_long long {  // 语法错误
    long long *data;
    size_t size;
}

4. Symbols: # and ##

The role of these two symbols in programming is also very clever, to say an exaggeration: in any framework code, you can see them!
The role is as follows:

  1. #: Convert the parameter to a string;
  2. ##: Connection parameters.

1. #: Stringification

Look directly at the simplest example:

#define STR(x) #x

printf("string of 123: %s \n", STR(123));

The input is a number 123 , and the output result is the string "123" , which is stringification.

2. ##: Parameter connection

The parameters in the macro are spliced according to characters to obtain a new identifier , for example:

#define MAKE_VAR(name, no) name##no

int main(void)
{
    int MAKE_VAR(a, 1) = 1; 
    int MAKE_VAR(b, 2) = 2; 

    printf("a1 = %d \n", a1);
    printf("b2 = %d \n", b2);
    return 0;
}

When the macro is called MAKE_VAR(a, 1), the symbol ## on both sides of the name and no replacement of a first 1 and then ligated to obtain A1 . Then the int data type in front of the call statement shows that a1 is an integer data, and finally it is initialized to 1.

Five, the processing of variable parameters

1. Definition and use of parameter names

The number of parameters defined by a macro can be undefined , just like calling the printf print function. When defining, you can use three dots (...) to represent variable parameters, or you can add the option before the three dots. The name of the variable parameter.

If you use three dots (...) to receive variable parameters, you need to use VA_ARGS to represent variable parameters when you use it , as follows:

#define debug1(...)      printf(__VA_ARGS__)

debug1("this is debug1: %d \n", 1);

If you add a parameter name in front of the three dots (...), you must use this parameter name when you use it, and you cannot use VA_ARGS to represent variable parameters , as follows:

#define debug2(args...)  printf(args)

debug1("this is debug2: %d \n", 2);

2. Processing with zero variable parameters

Take a look at this macro:

#define debug3(format, ...)      printf(format, __VA_ARGS__)

debug3("this is debug4: %d \n", 4);

There is no problem with compilation and execution. But if you use the macro like this:

debug3("hello \n");

Compile time error occurs: error: expected expression before ‘)’ token. why?

Take a look at the code after the macro expansion ( __VA_ARGS__empty):

printf("hello \n",);

See the problem, right? There is an extra comma after the format string ! In order to solve the problem, the preprocessor provides us with a method: use the ## symbol to automatically delete this extra comma . So there is no problem if the macro definition is changed to the following.

#define debug3(format, ...)     printf(format, ##__VA_ARGS__)

Similarly, if you define the name of a variable parameter yourself, add ## in front of it, as follows:

#define debug4(format, args...)  printf(format, ##args)

Sixth, the macro of a whimsical idea

The essence of macro expansion is text replacement , but once __VA_ARGS__the connection function of variable parameters ( ) and ## is added, infinite imagination can be changed .

I have always believed that imitation is the first step to becoming a master, only knowing more, seeing more, learning more how others use macros, and then using them for your own use, according to "first rigid-then optimize-finally solidify" This step is to train, and one day you can become a master.

Here we look at a few clever implementations using macro definitions.

1. Log function

Adding the log function to the code is almost standard for every product. Generally, the most common usage is the following:

#ifdef DEBUG
    #define LOG(...) printf(__VA_ARGS__)
#else
    #define LOG(...) 
#endif

int main()
{
    LOG("name = %s, age = %d \n", "zhangsan", 20);
    return 0;
}

When compiling, if you need to output the log function, pass in the macro definition DEBUG, so that the debugging information can be printed out. Of course, the actual product needs to be written into the file. If you do not need to print a statement, you can achieve the goal by defining the statement that prints the log information as an empty statement.

To put it another way, we can also control the printing information through conditional judgment statements , as follows:

#ifdef DEBUG
    #define debug if(1)
#else
     #define debug if(0)
#endif

int main()
{
    debug {
        printf("name = %s, age = %d \n", "zhangsan", 20);
    }
    return 0;
}

This control log information is not much to see, but it can also achieve the purpose. I put it here just to broaden your thinking.

2. Use macros to iterate each parameter

#define first(x, ...) #x
#define rest(x, ...)  #__VA_ARGS__

#define destructive(...)                              \
    do {                                              \
        printf("first is: %s\n", first(__VA_ARGS__)); \
        printf("rest are: %s\n", rest(__VA_ARGS__));  \
    } while (0)

int main(void)
{
    destructive(1, 2, 3);
    return 0;
}

The main idea is: each time the first parameter in the variable parameter VA_ARGS is separated, and then the following parameters are processed recursively , so that each parameter can be separated. I remember that teacher Hou Jie used the syntax of variable parameter templates in the C++ video screen to achieve a similar function.

I found the code demonstrated by Teacher Hou Jie in Youdao's notes just now. Friends who are familiar with C++ can study the following code:

// 递归的最后一次调用
void myprint()
{
}

template <typename T, typename... Types>
void myprint(const T &first, const Types&... args)
{
    std::cout << first << std::endl;
    std::cout << "remain args size = " << sizeof...(args) << std::endl;
   
    // 把其他参数递归调用
	myprint(args...);
}

int main()
{
    myprint("aaa", 7.5, 100);
    return 0;
}

3. Dynamically call different functions

// 普通的枚举类型
enum {
  ERR_One,
  ERR_Two,
  ERR_Three
};

// 利用 ## 的拼接功能,动态产生 case 中的比较值,以及函数名。
#define TEST(no) \
    case ERR_##no: \
      Func_##no(); \
      break;

void Func_One()
{
    printf("this is Func_One \n");
}

void Func_Two()
{
    printf("this is Func_Two \n");
}

void Func_Three()
{
    printf("this is Func_Three \n");
}

int main()
{
    int c = ERR_Two;
    switch (c) {
        TEST(One);
        TEST(Two);
        TEST(Three);
    };

    return 0;
}

In this example, the core is the TEST macro definition . Through the ## splicing function, the comparison target of the case branch is constructed, and then the corresponding function is dynamically spliced, and finally this function is called.

4. Dynamically create error codes and corresponding error strings

This is also a very clever example, using the two functions of #(stringification) and ##(splicing) to dynamically generate error code codes and corresponding error strings:

#define MY_ERRORS     \
    E(TOO_SMALL)      \
    E(TOO_BIG)        \
    E(INVALID_VARS)

#define E(e) Error_## e,
typedef enum {
    MY_ERRORS
} MyEnums;
#undef E

#define E(e) #e,
const char *ErrorStrings[] = {
    MY_ERRORS
};
#undef E

int main()
{
    printf("%d - %s \n", Error_TOO_SMALL, ErrorStrings[0]);
    printf("%d - %s \n", Error_TOO_BIG, ErrorStrings[1]);
    printf("%d - %s \n", Error_INVALID_VARS, ErrorStrings[2]);

    return 0;
}

After we expand the macro, we get an enumerated type and an array of string constants:

typedef enum {
    Error_TOO_SMALL,
    Error_TOO_BIG,
    Error_INVALID_VARS,
} MyEnums;

const char *ErrorStrings[] = {
    "TOO_SMALL",
    "TOO_BIG",
    "INVALID_VARS",
};

Is the code after macro expansion very simple? The results of compilation and execution are as follows:

0 - TOO_SMALL 
1 - TOO_BIG 
2 - INVALID_VARS 

Seven, summary

Some people are dying of Hong Ai, which is too much to abuse; while some people hate Hong Ai to the bone, and even use the word evil! In fact, to C , macros are just like kitchen knives to chefs and gangsters: good use can make the code structure simple and easy to maintain; bad use will introduce obscure syntax and bugs that are difficult to debug.

For us developers, it is enough to balance the execution efficiency of the program and the maintainability of the code.


Don't brag, don't hype, don't exaggerate, write every article carefully!
Welcome to forward, to share to friends around technology, Columbia Road, to express my heartfelt thanks! Forwarded recommended language has helped you to think it over:

This summary article summarized by Brother Dao was written very carefully, which is very helpful to my technical improvement. Good things to share!


[Original Statement]

OF: Columbia Road (public No.: The IOT of town things )
know almost: Columbia Road
station B: Share Columbia Road
Denver: Columbia Road share
CSDN: Columbia Road Share

Reprint: Welcome to reprint, but without the consent of the author, this statement must be retained, and the original link must be given in the article.



Recommended reading

Use setjmp and longjmp in the C language to implement exception capture and coroutine
C language pointers-from the underlying principles to fancy skills, with pictures and codes to help you explain
step by step analysis-how to use C to achieve object-oriented programming
original gdb The underlying debugging principle is so simple
. The things about encryption and certificates
go deep into the LUA scripting language, so that you can thoroughly understand the debugging principle.

Guess you like

Origin blog.csdn.net/u012296253/article/details/113731727