C/C++ Macro Programming Analysis
The purpose of this article is to explain the rules and implementation methods of macro programming in C/C++, so that you no longer fear seeing macros in code. I will first talk about the macro expansion rules mentioned in C++ Standard 14, and then observe macro expansion by modifying Clang's source code. Finally, based on this knowledge, we will discuss the implementation of macro programming.
All the code for this document is available here: Download, Online Demo.
Introduction
We can use the command gcc -P -E a.cpp -o a.cpp.i
to make the compiler only perform preprocessing on the file a.cpp
and save the results to a.cpp.i
.
First, let's take a look at some examples:
Recursive Reentrancy
Macro ITER
swaps the positions of arg0
and arg1
. After Macro expansion, it becomes ITER(2, 1)
.
It can be seen that the positions of arg0
and arg1
have been successfully swapped. Here, the macro is expanded once and only once, without further recursion. In other words, during the macro expansion process, it is not allowed to recursively re-enter itself. If it is found that the same macro has already been expanded in a previous recursion, it will not be expanded again. This is one of the important rules of macro expansion. The reason for prohibiting recursive re-entry is also simple, which is to avoid infinite recursion.
String concatenation
#define CONCAT(arg0, arg1) arg0 ## arg1
CONCAT(Hello, World) // -> HelloWorld
CONCAT(Hello, CONCAT(World, !)) // -> HelloCONCAT(World, !)
The purpose of the macro CONCAT
is to concatenate arg0
and arg1
. After expansion, CONCAT(Hello, World)
gives the correct result HelloWorld
. However, CONCAT(Hello, CONCAT(World, !))
only expands the outer macro, and the inner CONCAT(World, !)
is not expanded but concatenated directly with Hello
, which is different from what we expected. The desired result is HelloWorld!
. This is another important rule of macro expansion: macro parameters following the ##
operator are not expanded, but directly concatenated with the preceding content.
Through the two examples above, it can be seen that there are some counterintuitive rules for macro expansion. If we are not clear about the specific rules, it is possible to write macros that do not produce the desired effect.
Expansion rules
Through the two examples in the preamble, we learned that macro expansion has a set of standard rules. These rules are defined in the C/C++ standard and are not extensive in content. It is recommended to read them carefully a few times. By the way, here is a link to the standard n4296 version that includes the definition of macro expansion in section 16.3: Link. Below, I have selected several important rules from the n4296 version, which will determine how to correctly write macros (I recommend taking the time to carefully read and expand the macros in the standard).
Parameter Separation
The requirements for macro parameters are to be separated by commas, and the number of parameters needs to match the number of macro definitions. In the parameters passed to the macro, any content enclosed in parentheses is considered as one parameter, and parameters are allowed to be empty.
#define ADD_COMMA(arg1, arg2) arg1, arg2
ADD_COMMA(a, b) // -> a, b
ADD_COMMA(a) // Error: "macro "MACRO" requires 2 arguments, but only 1 given"
ADD_COMMA((a, b), c) // -> (a, b), c
ADD_COMMA(, b) // -> , b
In the expression ADD_COMMA((a, b), c)
, (a, b)
is considered the first argument. In ADD_COMMA(, b)
, the first argument is empty, so it expands to , b
.
Macro parameter expansion
When expanding macros, if the parameters of the macro can also be expanded macros, the parameters will be fully expanded first before expanding the macro. For example,
In general, the expansion of macros can be understood as evaluating the parameters first, and then evaluating the macros, unless the #
and ##
operators are encountered.
#
Operator
#
The macro parameter following the operator will not be expanded, it will be directly converted into a string, for example:
According to this rule, STRINGIZE(STRINGIZE(a))
can only be expanded as "STRINGIZE(a)"
.
Operators
Macro parameters before and after the operator will not be expanded, they will be directly concatenated, for example:
#define CONCAT(arg0, arg1) arg0 ## arg1
CONCAT(Hello, World) // -> HelloWorld
CONCAT(Hello, CONCAT(World, !)) // -> HelloCONCAT(World, !)
CONCAT(CONCAT(Hello, World) C, ONCAT(!)) // -> CONCAT(Hello, World) CONCAT(!)
CONCAT(CONCAT(Hello, World) C, ONCAT(!))
can only be concatenated together to obtain CONCAT(Hello, World) CONCAT(!)
.
Repetition scanning
After the preprocessor completes one macro expansion, it will re-scan the resulting content and continue expanding it until there is no more content to expand.
A macro expansion can be understood as first fully expanding the parameters (unless encountering #
and ##
), then replacing the macro and fully expanded parameters according to the definition of the macro, and finally handling all #
and ##
operators in the definition.
#define CONCAT(arg0, arg1) arg0 ## arg1
#define STRINGIZE(arg0) # arg0
CONCAT(STRING, IZE(Hello)) // -> STRINGIZE(Hello) -> "Hello"
CONCAT(STRING, IZE(Hello))
is expanded to STRINGIZE(Hello)
during the first scan, and then it goes through the second scan. STRINGIZE
is further expanded, resulting in "Hello"
.
No recursive reentrant calls allowed.
During the process of repeated scanning, it is forbidden to recursively expand the same macro. The expansion of macros can be understood as a tree-like structure, where the root node is the macro to be expanded initially, and the content after each macro expansion becomes a child node connected to the tree. Therefore, prohibiting recursion means that when expanding a child node macro, if it is the same as any ancestor node macro, expansion is prohibited. Let's look at some examples:
#define CONCAT(arg0, arg1) arg0 ## arg1
#define CONCAT_SPACE(arg0, arg1) arg0 arg1
#define IDENTITY(arg0) IDENTITY_IMPL(arg0)
#define IDENTITY_IMPL(arg0) arg0
CONCAT(CON, CAT(a, b)) // -> CONCAT(a, b)
IDENTITY_IMPL(CONCAT(CON, CAT(a, b))) // -> CONCAT(a, b)
IDENTITY(CONCAT(CON, CAT(a, b))) // -> IDENTITY_IMPL(CONCAT(a, b)) -> CONCAT(a, b)
CONCAT(CON, CAT(a, b))
: Since CONCAT
concatenates two parameters using ##
, according to the rules of ##
, the parameters will not be expanded, but directly concatenated. So the first expansion results in CONCAT(a, b)
. Since CONCAT
has already been expanded, it will not be recursively expanded again, so it stops.
IDENTITY_IMPL(CONCAT(CON, CAT(a, b)))
: IDENTITY_IMPL
can be understood as evaluating the argument arg0
. In this case, the argument arg0
evaluates to CONCAT(a, b)
. Since it is marked as non-reentrant due to recursion, IDENTITY_IMPL
completes its expansion. During the second scan, it encounters the non-reentrant CONCAT(a, b)
and stops expanding. Here, CONCAT(a, b)
is expanded from the argument arg0
. However, in subsequent expansions, the non-reentrant marking is maintained, which means that the parent node is the argument arg0
and the non-reentrant marking is always preserved.
IDENTITY(CONCAT(CON, CAT(a, b)))
: This example is primarily designed to enhance the understanding of parent-child nodes. When the parameter is expanded, it acts as the parent node, with the expanded content serving as the child node for recursive evaluation. Once the expanded parameter is passed to the macro definition, the flag for prohibiting reentry will continue to be retained (if the expanded macro does not alter the passed parameter). The process of expanding the parameter can be likened to another tree, with the expansion result of the parameter being the bottommost child node of the tree. This child node is passed to the macro for execution, while still preserving the characteristic of prohibiting reentry.
For example, here, after the first complete expansion, we obtain IDENTITY_IMPL(CONCAT(a, b))
. CONCAT(a, b)
is marked as non-reentrant, even though IDENTITY_IMPL
evaluates its arguments. Since the arguments are prohibited from expanding, they are passed as they are into the definition, and we ultimately get CONCAT(a, b)
.
Above I just listed some rules that I think are more important or rules that I find difficult to understand. For detailed macro expansion rules, I still recommend taking some time to directly refer to the standard documentation.
Observe the expansion process through Clang.
We can add some print statements to the Clang source code to observe the macro expansion process. I have no intention of delving into the explanation of the Clang source code here. Here is a modified file diff, for those who are interested, you can compile Clang yourself for research. Here I used LLVM version 11.1.0 (link),modified file (link}). Next, let's verify the macro expansion rules we introduced earlier through some simple examples:
Example 1
Preprocess the above code using a modified version of Clang: clang -P -E a.cpp -o a.cpp.i
. The following printout is obtained:
Chapter 1When the HandleIdentifier
function encounters a macro, it will print and then print the macro information (line 2-4(Macro is ok to expand), since macros are not disabled, we can proceed to expand the defined macros, EnterMacro
.
The function that actually performs the macro expansion is ExpandFunctionArguments
. After that, the macro information to be expanded is printed again, noting that the macro is now marked as used
(line 9After that, according to the definition of the macro, expand each Token
one by one (a Token
is a concept in the Clang
preprocessor, which will not be elaborated here).
The 0th Token
is the formal parameter arg0
, and its corresponding actual argument is C
. Since no expansion is required for the condition, it is directly copied to the result (lines 11-13行)。
(End).
The first Token
is hashhash
, which is the ##
operator, continue copying it to the result (from 14-15(Line).
(#__codelineno-9-16)行)。 行).
Finally, Leave ExpandFunctionArguments
prints the result obtained from expanding the scan this time (page 19In the code snippet above, the Token
that represents the result is translated as C##ONCAT(a, b)
. Then, the preprocessor executes the ##
operator to generate a new content.
After executing, we get CONCAT(a, b)
. When encountering the macro CONCAT
, the preprocessor still enters the HandleIdentifier
first. The macro information is printed and it is found that the macro status is disable used
, indicating that it has already been expanded and re-entry is prohibited. The message "Macro is not ok to expand" is displayed. The preprocessor no longer expands, and the final result obtained is CONCAT(a, b)
.
Example 2
#define CONCAT(arg0, arg1) arg0 ## arg1
#define IDENTITY(arg0) arg0
IDENTITY(CONCAT(C, ONCAT(a, b)))
Clang print information (click to expand):
12Start by expanding IDENTITY
. We discover that the Token 0
parameter is CONCAT(...)
, which is also a macro. Therefore, we evaluate this parameter first.
Chapter 27Start expanding the parameter macro CONCAT(...)
as in example 1. After multiple scans of expansion, it results in CONCAT(a, b)
(the 46th 46(行).
Chapter 47End the expansion of IDENTITY
and obtain the result CONCAT(a, b)
.
51Re-scan CONCAT(a, b)
and it is found that although it is a macro, it has already been set as used
during the previous parameter expansion process. It will no longer be recursively expanded and will directly serve as the final result.
Example 3
#define CONCAT(arg0, arg1) arg0 ## arg1
#define IDENTITY_IMPL(arg0) arg0
#define IDENTITY(arg0) IDENTITY_IMPL(arg0)
IDENTITY(CONCAT(C, ONCAT(a, b)))
Clang print information (click to expand):
- Chapter 16When processing starts, the preprocessor sees that
IDENTITY
is being expanded. Similarly, when it encountersToken 2
(which isarg0
), being a macro, it first expandsCONCAT(C, ONCAT(a, b))
.
Expand arg0
to obtain CONCAT(a, b)
(page 23-54行)
IDENTITY
ultimately expands toIDENTITY_IMPL(CONCAT(a, b))
(page 57(行)
- Rescan, continue expanding
IDENTITY_IMPL
(from 61-72(#__codelineno-13-85)行)。
(End of line).
Re-scanning the results, it was found that the status of the macro CONCAT(a, b)
is used
, so the expansion is stopped and the final result is obtained.
By using the three simple examples above, we can roughly understand the process of macro expansion by the preprocessor. We won't delve further into the preprocessor here, but if you're interested, you can study it by comparing the modified files I provided.
Macro programming implementation
Now let's move on to the topic (the previous paragraph was meant to provide a better understanding of macro expansion rules), macro programming implementation.
Basic Symbols
First, you can define the special symbols of macros, which will be used for evaluation and concatenation.
#define PP_LPAREN() (
#define PP_RPAREN() )
#define PP_COMMA() ,
#define PP_EMPTY()
#define PP_HASHHASH # ## # // Represents the string "##", without being treated as the ## operator
Evaluate
Using the rule of parameter precedence, you can write an evaluation macro:
#define PP_IDENTITY(arg0) arg0
PP_COMMA PP_LPAREN() PP_RPAREN() // -> PP_COMMA ( )
PP_IDENTITY(PP_COMMA PP_LPAREN() PP_RPAREN()) // -> PP_COMMA() -> ,
If you only write PP_COMMA PP_LPAREN() PP_RPAREN()
, the preprocessor will only process each macro separately, and will not merge the results of the expansion. With the addition of PP_IDENTITY
, the preprocessor can evaluate the expanded PP_COMMA()
to obtain ,
.
Splicing
Due to the fact that when ##
is used for concatenation, it does not expand the parameters on both sides, in order to allow the parameters to be evaluated before concatenation, you can write it like this:
#define PP_CONCAT(arg0, arg1) PP_CONCAT_IMPL(arg0, arg1)
#define PP_CONCAT_IMPL(arg0, arg1) arg0 ## arg1
PP_CONCAT(PP_IDENTITY(1), PP_IDENTITY(2)) // -> 12
`PP_CONCAT_IMPL(PP_IDENTITY(1), PP_IDENTITY(2)) // -> PP_IDENTITY(1)PP_IDENTITY(2) -> 报错`
`PP_CONCAT_IMPL(PP_IDENTITY(1), PP_IDENTITY(2)) // -> PP_IDENTITY(1)PP_IDENTITY(2) -> Error`
The method used here by PP_CONCAT
is called delayed concatenation. When it is expanded as PP_CONCAT_IMPL
, both arg0
and arg1
will be expanded and evaluated first, and then the actual concatenation operation will be performed by PP_CONCAT_IMPL
.
Logical Operations
With the help of PP_CONCAT
, logical operations can be achieved. First, define the BOOL
value:
#define PP_BOOL(arg0) PP_CONCAT(PP_BOOL_, arg0)
#define PP_BOOL_0 0
#define PP_BOOL_1 1
#define PP_BOOL_2 1
#define PP_BOOL_3 1
// ...
#define PP_BOOL_256 1
PP_BOOL(3) // -> PP_BOOL_3 -> 1
First, use PP_CONCAT
to concatenate PP_BOOL_
and arg0
together, and then evaluate the concatenated result. Here, arg0
is required to be a number within the range of [0, 256]
after evaluation. By evaluating the concatenation after PP_BOOL_
, we can obtain a boolean value. This applies to logical AND, OR, and NOT operations:
#define PP_NOT(arg0) PP_CONCAT(PP_NOT_, PP_BOOL(arg0))
#define PP_NOT_0 1
#define PP_NOT_1 0
#define PP_AND(arg0, arg1) PP_CONCAT(PP_AND_, PP_CONCAT(PP_BOOL(arg0), PP_BOOL(arg1)))
#define PP_AND_00 0
#define PP_AND_01 0
#define PP_AND_10 0
#define PP_AND_11 1
#define PP_OR(arg0, arg1) PP_CONCAT(PP_OR_, PP_CONCAT(PP_BOOL(arg0), PP_BOOL(arg1)))
#define PP_OR_00 0
#define PP_OR_01 1
#define PP_OR_10 1
#define PP_OR_11 1
PP_NOT(PP_BOOL(2)) // -> PP_CONCAT(PP_NOT_, 1) -> PP_NOT_1 -> 0
PP_AND(2, 3) // -> PP_CONCAT(PP_AND_, 11) -> PP_AND_11 -> 1
PP_AND(2, 0) // -> PP_CONCAT(PP_AND_, 10) -> PP_AND_10 -> 0
PP_OR(2, 0) // -> PP_CONCAT(PP_OR_, 10) -> PP_OR_10, -> 1
First, evaluate the parameter using PP_BOOL
, and then concatenate the results of logical operations based on the combination of 0
and 1
. If PP_BOOL
is not used for evaluation, the parameter will only support the two values 0
and 1
, greatly reducing its versatility. Similarly, you can also write XOR, OR, NOT operations. If interested, you can try it yourself.
Conditional selection
By utilizing PP_BOOL
and PP_CONCAT
, it is also possible to write conditional selection statements:
#define PP_IF(if, then, else) PP_CONCAT(PP_IF_, PP_BOOL(if))(then, else)
#define PP_IF_1(then, else) then
#define PP_IF_0(then, else) else
PP_IF(1, 2, 3) // -> PP_IF_1(2, 3) -> 2
PP_IF(0, 2, 3) // -> PP_IF_0(2, 3) -> 3
If the evaluation of if
is 1
, it is concatenated with PP_CONCAT
to form PP_IF_1
, and finally expanded to the value of then
. Similarly, if the evaluation of if
is 0
, PP_IF_0
is obtained.
Increment and decrement.
Integer Increment and Decrement:
#define PP_INC(arg0) PP_CONCAT(PP_INC_, arg0)
#define PP_INC_0 1
#define PP_INC_1 2
#define PP_INC_2 3
#define PP_INC_3 4
// ...
#define PP_INC_255 256
#define PP_INC_256 256
#define PP_DEC(arg0) PP_CONCAT(PP_DEC_, arg0)
#define PP_DEC_0 0
#define PP_DEC_1 0
#define PP_DEC_2 1
#define PP_DEC_3 2
// ...
#define PP_DEC_255 254
#define PP_DEC_256 255
PP_INC(2) // -> PP_INC_2 -> 3
PP_DEC(3) // -> PP_DEC_3 -> 2
Similar to PP_BOOL
, the increment and decrement of integers are also subject to range restrictions. Here, the range is set to [0, 256]
. After reaching 256
during the increment operation, for safety reasons, PP_INC_256
returns itself as the boundary value (256
). Similarly, PP_DEC_0
returns 0
.
Variable-length parameters
Macros can accept variable-length parameters, in the following format:
#define LOG(format, ...) printf("log: " format, __VA_ARGS__)
LOG("Hello %s\n", "World") // -> printf("log: " "Hello %s\n", "World");
LOG("Hello World") // -> printf("log: " "Hello World", ); There is an extra comma, causing a compilation error.
Because variable-length parameters may be empty, this can lead to compilation failure. Therefore, C++ 20 introduced __VA_OPT__
. If the variable-length parameters are empty, it returns empty; otherwise, it returns the original parameters:
#define LOG2(format, ...) printf("log: " format __VA_OPT__(,) __VA_ARGS__)
LOG2("Hello %s\n", "World") // -> printf("log: " "Hello %s\n", "World");
LOG2("Hello World") // -> printf("log: " "Hello World" ); No comma, compiles normally
But unfortunately, only standard C++ 20 and above have this macro. In the following sections, we will provide the implementation method of __VA_OPT__
.
Lazy evaluation
Consider this situation:
PP_IF(1, PP_COMMA(), PP_LPAREN()) // -> PP_IF_1(,,)) -> Error: unterminated argument list invoking macro "PP_IF_1"
We know that when macro expansion occurs, the first parameter is evaluated. After evaluating PP_COMMA()
and PP_LPAREN()
, they are passed to PP_IF_1
, resulting in PP_IF_1(,,))
, causing a preprocessing error. At this time, we can use a method called lazy evaluation:
Change it to this writing method, only pass the name of the macro, let PP_IF
select the desired macro name, then concatenate it with parentheses ()
to form a complete macro, and finally expand it. Lazy evaluation is also very common in macro programming.
Start with parentheses.
Check if the variable length parameters start with parentheses:
#define PP_IS_BEGIN_PARENS(...) \
PP_IS_BEGIN_PARENS_PROCESS( \
PP_IS_BEGIN_PARENS_CONCAT( \
PP_IS_BEGIN_PARENS_PRE_, PP_IS_BEGIN_PARENS_EAT __VA_ARGS__ \
) \
)
#define PP_IS_BEGIN_PARENS_PROCESS(...) PP_IS_BEGIN_PARENS_PROCESS_0(__VA_ARGS__)
#define PP_IS_BEGIN_PARENS_PROCESS_0(arg0, ...) arg0
#define PP_IS_BEGIN_PARENS_CONCAT(arg0, ...) PP_IS_BEGIN_PARENS_CONCAT_IMPL(arg0, __VA_ARGS__)
#define PP_IS_BEGIN_PARENS_CONCAT_IMPL(arg0, ...) arg0 ## __VA_ARGS__
#define PP_IS_BEGIN_PARENS_PRE_1 1,
#define PP_IS_BEGIN_PARENS_PRE_PP_IS_BEGIN_PARENS_EAT 0,
#define PP_IS_BEGIN_PARENS_EAT(...) 1
PP_IS_BEGIN_PARENS(()) // -> 1
PP_IS_BEGIN_PARENS((())) // -> 1
PP_IS_BEGIN_PARENS(a, b, c) // -> 0
PP_IS_BEGIN_PARENS(a, ()) // -> 0
PP_IS_BEGIN_PARENS(a()) // -> 0
PP_IS_BEGIN_PARENS(()aa(bb()cc)) // -> 1
PP_IS_BEGIN_PARENS(aa(bb()cc)) // -> 0
PP_IS_BEGIN_PARENS
can be used to determine whether the incoming parameters start with parentheses. It is needed when dealing with parentheses parameters (like the __VA_OPT__
implementation mentioned later). It looks a bit complicated, but the core idea is to construct a macro. If the variable-length parameters start with parentheses, they can be evaluated together with the parentheses to obtain one result. Otherwise, they can be evaluated separately to obtain another result. Let's take a closer look:
The macro composed of PP_IS_BEGIN_PARENS_PROCESS
and PP_IS_BEGIN_PARENS_PROCESS_0
is used to first evaluate the variable-length arguments that are passed in, and then retrieve the 0-th argument.
Translate these text into English language:
PP_IS_BEGIN_PARENS_CONCAT(PP_IS_BEGIN_PARENS_PRE_, PP_IS_BEGIN_PARENS_EAT __VA_ARGS__)
is evaluating PP_IS_BEGIN_PARENS_EAT __VA_ARGS__
first, then concatenating the evaluation result with PP_IS_BEGIN_PARENS_PRE_
.
The PP_IS_BEGIN_PARENS_EAT(...)
macro will consume all its parameters and return 1. If in the previous step, __VA_ARGS__
begins with parentheses, it will match the evaluation of PP_IS_BEGIN_PARENS_EAT(...)
, and then return 1. Conversely, if it does not begin with parentheses, there will be no match, and PP_IS_BEGIN_PARENS_EAT __VA_ARGS__
will remain unchanged.
If PP_IS_BEGIN_PARENS_EAT __VA_ARGS__
evaluates to 1
, PP_IS_BEGIN_PARENS_CONCAT(PP_IS_BEGIN_PARENS_PRE_, 1) -> PP_IS_BEGIN_PARENS_PRE_1 -> 1
, note that there is a comma after 1
, pass 1,
to PP_IS_BEGIN_PARENS_PROCESS_0
, take the 0th argument, and finally obtain 1
, which indicates that the parameter is starting with parentheses.
If PP_IS_BEGIN_PARENS_EAT __VA_ARGS__
evaluates to something other than 1
, but remains unchanged, then PP_IS_BEGIN_PARENS_CONCAT(PP_IS_BEGIN_PARENS_PRE_, PP_IS_BEGIN_PARENS_EAT __VA_ARGS__) -> PP_IS_BEGIN_PARENS_PRE_PP_IS_BEGIN_PARENS_EAT __VA_ARGS__ -> 0, __VA_ARGS__
, passed to PP_IS_BEGIN_PARENS_PROCESS_0
, will result in 0
, indicating that the argument does not begin with parentheses.
Variadic parameter void
Judging whether the variable-length parameter is empty is also a commonly used macro, which is needed when implementing __VA_OPT__
. Here, we can use PP_IS_BEGIN_PARENS
to write an incomplete version first:
#define PP_IS_EMPTY_PROCESS(...) \
PP_IS_BEGIN_PARENS(PP_IS_EMPTY_PROCESS_EAT __VA_ARGS__ ())
#define PP_IS_EMPTY_PROCESS_EAT(...) ()
PP_IS_EMPTY_PROCESS() // -> 1
PP_IS_EMPTY_PROCESS(1) // -> 0
PP_IS_EMPTY_PROCESS(1, 2) // -> 0
PP_IS_EMPTY_PROCESS(()) // -> 1
The function of PP_IS_EMPTY_PROCESS
is to determine whether PP_IS_EMPTY_PROCESS_EAT __VA_ARGS__ ()
begins with parentheses.
If __VA_ARGS__
is empty, PP_IS_EMPTY_PROCESS_EAT __VA_ARGS__ () -> PP_IS_EMPTY_PROCESS_EAT() -> ()
, it results in a pair of parentheses ()
, which is then passed to PP_IS_BEGIN_PARENS
returning 1
, indicating that the argument is empty.
Otherwise, PP_IS_EMPTY_PROCESS_EAT __VA_ARGS__ ()
is passed unchanged to PP_IS_BEGIN_PARENS
, which returns 0 indicating non-empty.
Pay attention to the 4th example PP_IS_EMPTY_PROCESS(()) -> 1
, PP_IS_EMPTY_PROCESS
cannot handle variable arguments that start with parentheses correctly, because in this case, the parentheses brought by the variable arguments will match PP_IS_EMPTY_PROCESS_EAT
, resulting in the evaluation of ()
. To solve this problem, we need to handle the case where the arguments start with parentheses differently:
#define PP_IS_EMPTY(...) \
PP_IS_EMPTY_IF(PP_IS_BEGIN_PARENS(__VA_ARGS__)) \
(PP_IS_EMPTY_ZERO, PP_IS_EMPTY_PROCESS)(__VA_ARGS__)
#define PP_IS_EMPTY_IF(if) PP_CONCAT(PP_IS_EMPTY_IF_, if)
#define PP_IS_EMPTY_IF_1(then, else) then
#define PP_IS_EMPTY_IF_0(then, else) else
#define PP_IS_EMPTY_ZERO(...) 0
PP_IS_EMPTY() // -> 1
PP_IS_EMPTY(1) // -> 0
PP_IS_EMPTY(1, 2) // -> 0
PP_IS_EMPTY(()) // -> 0
PP_IS_EMPTY_IF
returns the 0th or 1st argument based on the if
condition.
If the variable-length parameters passed in start with parentheses, PP_IS_EMPTY_IF
returns PP_IS_EMPTY_ZERO
, and finally returns 0
, indicating that the variable-length parameters are not empty.
On the contrary, PP_IS_EMPTY_IF
returns PP_IS_EMPTY_PROCESS
, and finally PP_IS_EMPTY_PROCESS
is responsible for determining whether the variable-length parameters are empty.
Subscript access
Get the element at the specified position in a variable-length parameter:
#define PP_ARGS_ELEM(I, ...) PP_CONCAT(PP_ARGS_ELEM_, I)(__VA_ARGS__)
#define PP_ARGS_ELEM_0(a0, ...) a0
#define PP_ARGS_ELEM_1(a0, a1, ...) a1
#define PP_ARGS_ELEM_2(a0, a1, a2, ...) a2
#define PP_ARGS_ELEM_3(a0, a1, a2, a3, ...) a3
// ...
#define PP_ARGS_ELEM_7(a0, a1, a2, a3, a4, a5, a6, a7, ...) a7
#define PP_ARGS_ELEM_8(a0, a1, a2, a3, a4, a5, a6, a7, a8, ...) a8
PP_ARGS_ELEM(0, "Hello", "World") // -> PP_ARGS_ELEM_0("Hello", "World") -> "Hello"
PP_ARGS_ELEM(1, "Hello", "World") // -> PP_ARGS_ELEM_1("Hello", "World") -> "World"
The first argument of PP_ARGS_ELEM
is the index I
, followed by a variable number of arguments. By using PP_CONCAT
to concatenate PP_ARGS_ELEM_
and I
, we can obtain the macro PP_ARGS_ELEM_0..8
that returns the element at the respective position. Then we pass the variable arguments to this macro, expanding it to return the element corresponding to the index.
PP_IS_EMPTY2
You can also achieve another version of PP_IS_EMPTY
by using PP_ARGS_ELEM
:
#define PP_IS_EMPTY2(...) \
PP_AND( \
PP_AND( \
PP_NOT(PP_HAS_COMMA(__VA_ARGS__)), \
PP_NOT(PP_HAS_COMMA(__VA_ARGS__())) \
), \
PP_AND( \
PP_NOT(PP_HAS_COMMA(PP_COMMA_ARGS __VA_ARGS__)), \
PP_HAS_COMMA(PP_COMMA_ARGS __VA_ARGS__ ()) \
) \
)
#define PP_HAS_COMMA(...) PP_ARGS_ELEM(8, __VA_ARGS__, 1, 1, 1, 1, 1, 1, 1, 0)
#define PP_COMMA_ARGS(...) ,
PP_IS_EMPTY2() // -> 1
PP_IS_EMPTY2(a) // -> 0
PP_IS_EMPTY2(a, b) // -> 0
PP_IS_EMPTY2(()) // -> 0
PP_IS_EMPTY2(PP_COMMA) // -> 0
Use PP_ARGS_ELEM
to implement the judgement of whether the argument contains a comma PP_HAS_COMMA
. PP_COMMA_ARGS
will consume any passed arguments and return a comma.
The basic logic to determine if the variable-length parameters are empty is PP_COMMA_ARGS __VA_ARGS__ ()
, which returns a comma, meaning __VA_ARGS__
is empty. PP_COMMA_ARGS
and ()
are concatenated and evaluated together, specifically written as PP_HAS_COMMA(PP_COMMA_ARGS __VA_ARGS__ ())
.
But there will be exceptions:
The __VA_ARGS__
itself may contain commas.
* __VA_ARGS__ ()
concatenates the arguments causing an evaluation with a comma.
* PP_COMMA_ARGS __VA_ARGS__
concatenates and evaluates with a comma occurring in-between;
In order to address the three exceptional scenarios mentioned above, exclusions need to be made. Therefore, the final expression is equivalent to executing the AND logic on the following four conditions:
PP_NOT(PP_HAS_COMMA(__VA_ARGS__))
&&PP_NOT(PP_HAS_COMMA(__VA_ARGS__()))
&&PP_NOT(PP_HAS_COMMA(PP_COMMA_ARGS __VA_ARGS__))
&&PP_HAS_COMMA(PP_COMMA_ARGS __VA_ARGS__ ())
__VA_OPT__
Finally, with the help of PP_IS_EMPTY
, it is possible to implement macros similar to __VA_OPT__
.
#define PP_REMOVE_PARENS(tuple) PP_REMOVE_PARENS_IMPL tuple
#define PP_REMOVE_PARENS_IMPL(...) __VA_ARGS__
#define PP_ARGS_OPT(data_tuple, empty_tuple, ...) \
PP_ARGS_OPT_IMPL(PP_IF(PP_IS_EMPTY(__VA_ARGS__), empty_tuple, data_tuple))
#define PP_ARGS_OPT_IMPL(tuple) PP_REMOVE_PARENS(tuple)
PP_ARGS_OPT((data), (empty)) // -> empty
PP_ARGS_OPT((data), (empty), 1) // -> data
PP_ARGS_OPT((,), (), 1) // -> ,
PP_ARGS_OPT
accepts two fixed parameters and a variable number of parameters. If the variable parameters are not empty, it returns data
; otherwise, it returns empty
. To support commas in data
and empty
, both need to be enclosed in parentheses with the actual parameters, and finally use PP_REMOVE_PARENS
to remove the outer parentheses.
With PP_ARGS_OPT
, it is possible to achieve the functionality of LOG2
through the simulation of LOG3
.
#define LOG3(format, ...) \
printf("log: " format PP_ARGS_OPT((,), (), __VA_ARGS__) __VA_ARGS__)
LOG3("Hello"); // -> printf("log: " "Hello" );
LOG3("Hello %s", "World"); // -> printf("log: " "Hello %s" , "World");
data_tuple
is (,)
. If the variable-length argument is not empty, it will return all the elements in data_tuple
, which in this case is a comma ,
.
Number of parameters needed
Obtaining the quantity of variable arguments:
#define PP_ARGS_SIZE_IMCOMPLETE(...) \
PP_ARGS_ELEM(8, __VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
PP_ARGS_SIZE_IMCOMPLETE(a) // -> 1
PP_ARGS_SIZE_IMCOMPLETE(a, b) // -> 2
PP_ARGS_SIZE_IMCOMPLETE(PP_COMMA()) // -> 2
PP_ARGS_SIZE_IMCOMPLETE() // -> 1
The number of variable-length parameters is obtained by counting the positions of the parameters. __VA_ARGS__
will cause all subsequent parameters to shift to the right. Use the macro PP_ARGS_ELEM
to retrieve the parameter in the 8th position. If __VA_ARGS__
only has one parameter, then the 8th parameter is equal to 1
. Similarly, if __VA_ARGS__
has two parameters, then the 8th parameter will be 2
, which conveniently represents the number of variable-length parameters.
The examples given here only support a maximum number of 8 variable arguments, which is dependent on the maximum length supported by PP_ARGS_ELEM
.
But this macro is still incomplete. In the case where the variable argument is empty, this macro will mistakenly return 1
. If you need to handle empty variable arguments, you will need to use the PP_ARGS_OPT
macro we mentioned earlier.
#define PP_COMMA_IF_ARGS(...) PP_ARGS_OPT((,), (), __VA_ARGS__)
#define PP_ARGS_SIZE(...) PP_ARGS_ELEM(8, __VA_ARGS__ PP_COMMA_IF_ARGS(__VA_ARGS__) 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0)
PP_ARGS_SIZE(a) // -> 1
PP_ARGS_SIZE(a, b) // -> 2
PP_ARGS_SIZE(PP_COMMA()) // -> 2
PP_ARGS_SIZE() // -> 0
PP_ARGS_SIZE(,,,) // -> 4
The key issue here is the comma ,
. When __VA_ARGS__
is empty, removing the comma will enable the correct return value of 0
.
Traversal Access
Similar to C++ for_each
, we can implement the macro PP_FOR_EACH
:
#define PP_FOR_EACH(macro, contex, ...) \
PP_CONCAT(PP_FOR_EACH_, PP_ARGS_SIZE(__VA_ARGS__))(0, macro, contex, __VA_ARGS__)
#define PP_FOR_EACH_0(index, macro, contex, ...)
#define PP_FOR_EACH_1(index, macro, contex, arg, ...) macro(index, contex, arg)
#define PP_FOR_EACH_2(index, macro, contex, arg, ...) \
macro(index, contex, arg) \
PP_FOR_EACH_1(PP_INC(index), macro, contex, __VA_ARGS__)
#define PP_FOR_EACH_3(index, macro, contex, arg, ...) \
macro(index, contex, arg) \
PP_FOR_EACH_2(PP_INC(index), macro, contex, __VA_ARGS__)
// ...
#define PP_FOR_EACH_8(index, macro, contex, arg, ...) \
macro(index, contex, arg) \
PP_FOR_EACH_7(PP_INC(index), macro, contex, __VA_ARGS__)
#define DECLARE_EACH(index, contex, arg) PP_IF(index, PP_COMMA, PP_EMPTY)() contex arg
PP_FOR_EACH(DECLARE_EACH, int, x, y, z); // -> int x, y, z;
PP_FOR_EACH(DECLARE_EACH, bool, a, b); // -> bool a, b;
PP_FOR_EACH
accepts two fixed parameters: macro
, which can be understood as the macro called during iteration, and contex
, which can be passed as a constant parameter to macro
. First, PP_FOR_EACH
uses PP_ARGS_SIZE
to obtain the length N
of the variable-length parameters, and then concatenates it with PP_CONCAT
to obtain PP_FOR_EACH_N
. Afterwards, PP_FOR_EACH_N
will iteratively call PP_FOR_EACH_N-1
to achieve the same number of iterations as the number of variable-length parameters.
In the example, we declare DECLARE_EACH
as a macro
parameter. The purpose of DECLARE_EACH
is to return contex arg
, where contex
is the name of a type and arg
is the name of a variable. By using DECLARE_EACH
, we can declare variables.
Conditional loop
With FOR_EACH
, we can also use a similar syntax to write PP_WHILE
:
#define PP_WHILE PP_WHILE_1
#define PP_WHILE_1(pred, op, val) PP_WHILE_1_IMPL(PP_BOOL(pred(val)), pred, op, val)
#define PP_WHILE_1_IMPL(cond, pred, op, val) \
PP_IF(cond, PP_WHILE_2, val PP_EMPTY_EAT)(pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
#define PP_WHILE_2(pred, op, val) PP_WHILE_2_IMPL(PP_BOOL(pred(val)), pred, op, val)
#define PP_WHILE_2_IMPL(cond, pred, op, val) \
PP_IF(cond, PP_WHILE_3, val PP_EMPTY_EAT)(pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
#define PP_WHILE_3(pred, op, val) PP_WHILE_3_IMPL(PP_BOOL(pred(val)), pred, op, val)
#define PP_WHILE_3_IMPL(cond, pred, op, val) \
PP_IF(cond, PP_WHILE_4, val PP_EMPTY_EAT)(pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
#define PP_WHILE_4(pred, op, val) PP_WHILE_4_IMPL(PP_BOOL(pred(val)), pred, op, val)
#define PP_WHILE_4_IMPL(cond, pred, op, val) \
PP_IF(cond, PP_WHILE_5, val PP_EMPTY_EAT)(pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
// ...
#define PP_WHILE_8(pred, op, val) PP_WHILE_8_IMPL(PP_BOOL(pred(val)), pred, op, val)
#define PP_WHILE_8_IMPL(cond, pred, op, val) \
PP_IF(cond, PP_WHILE_8, val PP_EMPTY_EAT)(pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
#define PP_EMPTY_EAT(...)
#define SUM_OP(xy_tuple) SUM_OP_OP_IMPL xy_tuple
#define SUM_OP_OP_IMPL(x, y) (PP_DEC(x), y + x)
#define SUM_PRED(xy_tuple) SUM_PRED_IMPL xy_tuple
#define SUM_PRED_IMPL(x, y) x
#define SUM(max_num, origin_num) \
PP_IDENTITY(SUM_IMPL PP_WHILE(SUM_PRED, SUM_OP, (max_num, origin_num)))
#define SUM_IMPL(ignore, ret) ret
PP_WHILE(SUM_PRED, SUM_OP, (2, a)) // -> (0, a + 2 + 1)
SUM(2, a) // -> a + 2 + 1
The PP_WHILE
macro accepts three parameters: pred
as the condition evaluation function, op
as the operation function, and val
as the initial value. During the looping process, the termination condition is continuously checked using pred(val)
. The value obtained from op(val)
is passed to the subsequent macros, which can be understood as executing the following code:
PP_WHILE_N
first uses pred(val)
to obtain the condition evaluation result, and then passes the condition result cond
and the remaining parameters to PP_WHILE_N_IMPL
.
The PP_WHILE_N_IMPL
can be divided into two parts: the latter half (pred, op, PP_IF(cond, op, PP_EMPTY_EAT)(val))
is used as the parameter for the former half, and PP_IF(cond, op, PP_EMPTY_EAT)(val)
evaluates op(val)
if cond
is true, otherwise it evaluates PP_EMPTY_EAT(val)
to obtain an empty result. The former half, PP_IF(cond, PP_WHILE_N+1, val PP_EMPTY_EAT)
, returns PP_WHILE_N+1
if cond
is true, and continues the loop by combining it with the parameters of the latter half. Otherwise, it returns val PP_EMPTY_EAT
, where val
becomes the final result, and PP_EMPTY_EAT
consumes the result of the latter half.
SUM
implements N + N-1 + ... + 1
. The initial values are (max_num, origin_num)
; SUM_PRED
takes the first element of the value, x
, and checks if it is greater than 0; SUM_OP
performs the decrement operation on x
, x = x - 1
, and the addition operation on y
, y = y + x
. We directly pass SUM_PRED
and SUM_OP
to PP_WHILE
, and the result returned is a tuple. The desired result is the second element of the tuple, so we then use SUM
to retrieve the value of the second element.
Recursive reentry
So far, our traversal visits and conditional loops have been working well and producing the expected results. Do you remember when we mentioned the prohibition of recursive reentry when discussing macro expansion rules? Unfortunately, we have encountered this prohibition of recursive reentry when we wanted to perform nested loops.
#define SUM_OP2(xy_tuple) SUM_OP_OP_IMPL2 xy_tuple
#define SUM_OP_OP_IMPL2(x, y) (PP_DEC(x), y + SUM(x, 0))
#define SUM2(max_num, origin_num) \
PP_IDENTITY(SUM_IMPL PP_WHILE(SUM_PRED, SUM_OP2, (max_num, origin_num)))
SUM2(1, a) // -> a + SUM_IMPL PP_WHILE_1(SUM_PRED, SUM_OP, (1, a))
SUM2
changes the parameter op
to SUM_OP2
, and inside SUM_OP2
, it will call SUM
, and the expansion of SUM
will be PP_WHILE_1
, which is effectively a recursive call to itself. This causes the preprocessor to stop expanding.
To solve this problem, we can use a method called Automatic Recursion:
#define PP_AUTO_WHILE PP_CONCAT(PP_WHILE_, PP_AUTO_REC(PP_WHILE_PRED))
#define PP_AUTO_REC(check) PP_IF(check(2), PP_AUTO_REC_12, PP_AUTO_REC_34)(check)
#define PP_AUTO_REC_12(check) PP_IF(check(1), 1, 2)
#define PP_AUTO_REC_34(check) PP_IF(check(3), 3, 4)
#define PP_WHILE_PRED(n) \
PP_CONCAT(PP_WHILE_CHECK_, PP_WHILE_ ## n(PP_WHILE_FALSE, PP_WHILE_FALSE, PP_WHILE_FALSE))
#define PP_WHILE_FALSE(...) 0
#define PP_WHILE_CHECK_PP_WHILE_FALSE 1
#define PP_WHILE_CHECK_PP_WHILE_1(...) 0
#define PP_WHILE_CHECK_PP_WHILE_2(...) 0
#define PP_WHILE_CHECK_PP_WHILE_3(...) 0
#define PP_WHILE_CHECK_PP_WHILE_4(...) 0
// ...
#define PP_WHILE_CHECK_PP_WHILE_8(...) 0
PP_AUTO_WHILE // -> PP_WHILE_1
#define SUM3(max_num, origin_num) \
PP_IDENTITY(SUM_IMPL PP_AUTO_WHILE(SUM_PRED, SUM_OP, (max_num, origin_num)))
#define SUM_OP4(xy_tuple) SUM_OP_OP_IMPL4 xy_tuple
#define SUM_OP_OP_IMPL4(x, y) (PP_DEC(x), y + SUM3(x, 0))
#define SUM4(max_num, origin_num) \
PP_IDENTITY(SUM_IMPL PP_AUTO_WHILE(SUM_PRED, SUM_OP4, (max_num, origin_num)))
SUM4(2, a) // -> a + 0 + 2 + 1 + 0 + 1
PP_AUTO_WHILE
is the automatically deduced recursive version of PP_WHILE
, and the central macro is PP_AUTO_REC(PP_WHILE_PRED)
. This macro can determine the current available PP_WHILE_N
version number, N
.
The principle of deduction is very simple, it is to search all versions and find the version that can be correctly expanded, returning the number of that version. In order to improve the speed of the search, the usual practice is to use binary search, which is what PP_AUTO_REC
is doing. PP_AUTO_REC
accepts a parameter called check
, which is responsible for checking the availability of versions. Here, the supported range of versions to be searched is given as [1, 4]
. PP_AUTO_REC
will first check check(2)
. If check(2)
is true, it will call PP_AUTO_REC_12
to search the range [1, 2]
; otherwise, it will use PP_AUTO_REC_34
to search [3, 4]
. PP_AUTO_REC_12
checks check(1)
. If it is true, it means version 1
is available; otherwise, version 2
is used. Similarly, PP_AUTO_REC_34
operates.
To write the check
macro and determine whether the version is available, how should we do it? Here, PP_WHILE_PRED
will be expanded into two parts of concatenation. Let's take a look at the latter part, PP_WHILE_ ## n(PP_WHILE_FALSE, PP_WHILE_FALSE, PP_WHILE_FALSE)
: if PP_WHILE_ ## n
is available, since PP_WHILE_FALSE
always returns 0
, this part will be expanded to obtain the value of the val
parameter, which is PP_WHILE_FALSE
; otherwise, this macro will remain unchanged and still be PP_WHILE_n(PP_WHILE_FALSE, PP_WHILE_FALSE, PP_WHILE_FALSE)
.
Join the result of the back section with the front section PP_WHILE_CHECK_
, and obtain two possible results: PP_WHILE_CHECK_PP_WHILE_FALSE
or PP_WHILE_CHECK_PP_WHILE_n(PP_WHILE_FALSE, PP_WHILE_FALSE, PP_WHILE_FALSE)
. Therefore, we let PP_WHILE_CHECK_PP_WHILE_FALSE
return 1
to indicate availability, and PP_WHILE_CHECK_PP_WHILE_n
return 0
to indicate unavailability. With this, we have completed the functionality of automatic recursive deduction.
Arithmetic Comparison
Not equal:
#define PP_NOT_EQUAL(x, y) PP_NOT_EQUAL_IMPL(x, y)
#define PP_NOT_EQUAL_IMPL(x, y) \
PP_CONCAT(PP_NOT_EQUAL_CHECK_, PP_NOT_EQUAL_ ## x(0, PP_NOT_EQUAL_ ## y))
#define PP_NOT_EQUAL_CHECK_PP_EQUAL_NIL 1
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_0(...) 0
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_1(...) 0
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_2(...) 0
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_3(...) 0
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_4(...) 0
// ...
#define PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_8(...) 0
#define PP_NOT_EQUAL_0(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
#define PP_NOT_EQUAL_1(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
#define PP_NOT_EQUAL_2(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
#define PP_NOT_EQUAL_3(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
#define PP_NOT_EQUAL_4(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
// ...
#define PP_NOT_EQUAL_8(cond, y) PP_IF(cond, PP_EQUAL_NIL, y(1, PP_EQUAL_NIL))
PP_NOT_EQUAL(1, 1) // -> 0
PP_NOT_EQUAL(3, 1) // -> 1
The judgment of whether two values are equal utilizes the feature of prohibiting recursive reentry. It combines x
and y
recursively to form the macro PP_NOT_EQUAL_x PP_NOT_EQUAL_y
. If x == y
, the macro PP_NOT_EQUAL_y
will not be expanded, and it will be concatenated with PP_NOT_EQUAL_CHECK_
to form PP_NOT_EQUAL_CHECK_PP_NOT_EQUAL_y
which returns 0
. On the other hand, if both expansions are successful, PP_EQUAL_NIL
will be obtained and concatenated with PP_NOT_EQUAL_CHECK_
to form PP_NOT_EQUAL_CHECK_PP_EQUAL_NIL
, which returns 1
.
Equal:
Less than or equal to:
#define PP_LESS_EQUAL(x, y) PP_NOT(PP_SUB(x, y))
PP_LESS_EQUAL(2, 1) // -> 0
PP_LESS_EQUAL(1, 1) // -> 1
PP_LESS_EQUAL(1, 2) // -> 1
Less than:
#define PP_LESS(x, y) PP_AND(PP_LESS_EQUAL(x, y), PP_NOT_EQUAL(x, y))
PP_LESS(2, 1) // -> 0
PP_LESS(1, 2) // -> 1
PP_LESS(2, 2) // -> 0
In addition, there are arithmetic comparisons like "greater than" and "greater than or equal to," among others. I won't go into detail about them here.
Arithmetic operations
With PP_AUTO_WHILE
, we can achieve basic arithmetic operations and even nested calculations.
Addition:
#define PP_ADD(x, y) \
PP_IDENTITY(PP_ADD_IMPL PP_AUTO_WHILE(PP_ADD_PRED, PP_ADD_OP, (x, y)))
#define PP_ADD_IMPL(x, y) x
#define PP_ADD_PRED(xy_tuple) PP_ADD_PRED_IMPL xy_tuple
#define PP_ADD_PRED_IMPL(x, y) y
#define PP_ADD_OP(xy_tuple) PP_ADD_OP_IMPL xy_tuple
#define PP_ADD_OP_IMPL(x, y) (PP_INC(x), PP_DEC(y))
PP_ADD(1, 2) // -> 3
PP_ADD(1, PP_ADD(1, 2)) // -> 4
Subtraction:
#define PP_SUB(x, y) \
PP_IDENTITY(PP_SUB_IMPL PP_AUTO_WHILE(PP_SUB_PRED, PP_SUB_OP, (x, y)))
#define PP_SUB_IMPL(x, y) x
#define PP_SUB_PRED(xy_tuple) PP_SUB_PRED_IMPL xy_tuple
#define PP_SUB_PRED_IMPL(x, y) y
#define PP_SUB_OP(xy_tuple) PP_SUB_OP_IMPL xy_tuple
#define PP_SUB_OP_IMPL(x, y) (PP_DEC(x), PP_DEC(y))
PP_SUB(2, 1) // -> 1
PP_SUB(3, PP_ADD(2, 1)) // -> 0
Multiplication:
#define PP_MUL(x, y) \
IDENTITY(PP_MUL_IMPL PP_AUTO_WHILE(PP_MUL_PRED, PP_MUL_OP, (0, x, y)))
#define PP_MUL_IMPL(ret, x, y) ret
#define PP_MUL_PRED(rxy_tuple) PP_MUL_PRED_IMPL rxy_tuple
#define PP_MUL_PRED_IMPL(ret, x, y) y
#define PP_MUL_OP(rxy_tuple) PP_MUL_OP_IMPL rxy_tuple
#define PP_MUL_OP_IMPL(ret, x, y) (PP_ADD(ret, x), x, PP_DEC(y))
PP_MUL(1, 1) // -> 1
PP_MUL(2, PP_ADD(0, 1)) // -> 2
The multiplication implementation here adds a parameter ret
, initialized to 0
, and each iteration executes ret = ret + x
.
Division:
#define PP_DIV(x, y) \
IDENTITY(PP_DIV_IMPL PP_AUTO_WHILE(PP_DIV_PRED, PP_DIV_OP, (0, x, y)))
#define PP_DIV_IMPL(ret, x, y) ret
#define PP_DIV_PRED(rxy_tuple) PP_DIV_PRED_IMPL rxy_tuple
#define PP_DIV_PRED_IMPL(ret, x, y) PP_LESS_EQUAL(y, x)
#define PP_DIV_OP(rxy_tuple) PP_DIV_OP_IMPL rxy_tuple
#define PP_DIV_OP_IMPL(ret, x, y) (PP_INC(ret), PP_SUB(x, y), y)
PP_DIV(1, 2) // -> 0
PP_DIV(2, 1) // -> 2
PP_DIV(2, PP_ADD(1, 1)) // -> 1
Division utilizes PP_LESS_EQUAL
, so the loop continues only if y <= x
.
Data structures
Macros can also have data structures. In fact, we have already used a data structure called tuple
earlier, where PP_REMOVE_PARENS
can remove the outer parentheses of a tuple
and return its elements. We will use tuple
as an example here to discuss the relevant implementation. If you are interested in other data structures such as list
, array
, etc., you can check out the implementation in Boost
.
A tuple
is defined as a collection of elements enclosed in parentheses and separated by commas: (a, b, c)
.
#define PP_TUPLE_REMOVE_PARENS(tuple) PP_REMOVE_PARENS(tuple)
// Get the element at the specified index
#define PP_TUPLE_ELEM(i, tuple) PP_ARGS_ELEM(i, PP_TUPLE_REMOVE_PARENS(tuple))
// Swallow the entire tuple and return empty
#define PP_TUPLE_EAT() PP_EMPTY_EAT
// Get size
#define PP_TUPLE_SIZE(tuple) PP_ARGS_SIZE(PP_TUPLE_REMOVE_PARENS(tuple))
// Add Element
#define PP_TUPLE_PUSH_BACK(elem, tuple) \
PP_TUPLE_PUSH_BACK_IMPL(PP_TUPLE_SIZE(tuple), elem, tuple)
#define PP_TUPLE_PUSH_BACK_IMPL(size, elem, tuple) \
(PP_TUPLE_REMOVE_PARENS(tuple) PP_IF(size, PP_COMMA, PP_EMPTY)() elem)
// Insert element
#define PP_TUPLE_INSERT(i, elem, tuple) \
PP_TUPLE_ELEM( \
3, \
PP_AUTO_WHILE( \
PP_TUPLE_INSERT_PRED, \
PP_TUPLE_INSERT_OP, \
(0, i, elem, (), tuple) \
) \
)
#define PP_TUPLE_INSERT_PRED(args) PP_TUPLE_INSERT_PERD_IMPL args
#define PP_TUPLE_INSERT_PERD_IMPL(curi, i, elem, ret, tuple) \
PP_NOT_EQUAL(PP_TUPLE_SIZE(ret), PP_INC(PP_TUPLE_SIZE(tuple)))
#define PP_TUPLE_INSERT_OP(args) PP_TUPLE_INSERT_OP_IMPL args
#define PP_TUPLE_INSERT_OP_IMPL(curi, i, elem, ret, tuple) \
( \
PP_IF(PP_NOT_EQUAL(PP_TUPLE_SIZE(ret), i), PP_INC(curi), curi), \
i, elem, \
PP_TUPLE_PUSH_BACK(\
PP_IF( \
PP_NOT_EQUAL(PP_TUPLE_SIZE(ret), i), \
PP_TUPLE_ELEM(curi, tuple), elem \
), \
ret \
), \
tuple \
)
// Delete the last element
#define PP_TUPLE_POP_BACK(tuple) \
PP_TUPLE_ELEM( \
1, \
PP_AUTO_WHILE( \
PP_TUPLE_POP_BACK_PRED, \
PP_TUPLE_POP_BACK_OP, \
(0, (), tuple) \
) \
)
#define PP_TUPLE_POP_BACK_PRED(args) PP_TUPLE_POP_BACK_PRED_IMPL args
#define PP_TUPLE_POP_BACK_PRED_IMPL(curi, ret, tuple) \
PP_IF( \
PP_TUPLE_SIZE(tuple), \
PP_NOT_EQUAL(PP_TUPLE_SIZE(ret), PP_DEC(PP_TUPLE_SIZE(tuple))), \
0 \
)
#define PP_TUPLE_POP_BACK_OP(args) PP_TUPLE_POP_BACK_OP_IMPL args
#define PP_TUPLE_POP_BACK_OP_IMPL(curi, ret, tuple) \
(PP_INC(curi), PP_TUPLE_PUSH_BACK(PP_TUPLE_ELEM(curi, tuple), ret), tuple)
// Remove Element
#define PP_TUPLE_REMOVE(i, tuple) \
PP_TUPLE_ELEM( \
2, \
PP_AUTO_WHILE( \
PP_TUPLE_REMOVE_PRED, \
PP_TUPLE_REMOVE_OP, \
(0, i, (), tuple) \
) \
)
#define PP_TUPLE_REMOVE_PRED(args) PP_TUPLE_REMOVE_PRED_IMPL args
#define PP_TUPLE_REMOVE_PRED_IMPL(curi, i, ret, tuple) \
PP_IF( \
PP_TUPLE_SIZE(tuple), \
PP_NOT_EQUAL(PP_TUPLE_SIZE(ret), PP_DEC(PP_TUPLE_SIZE(tuple))), \
0 \
)
#define PP_TUPLE_REMOVE_OP(args) PP_TUPLE_REMOVE_OP_IMPL args
#define PP_TUPLE_REMOVE_OP_IMPL(curi, i, ret, tuple) \
( \
PP_INC(curi), \
i, \
PP_IF( \
PP_NOT_EQUAL(curi, i), \
PP_TUPLE_PUSH_BACK(PP_TUPLE_ELEM(curi, tuple), ret), \
ret \
), \
tuple \
)
PP_TUPLE_SIZE(()) // -> 0
PP_TUPLE_PUSH_BACK(2, (1)) // -> (1, 2)
PP_TUPLE_PUSH_BACK(2, ()) // -> (2)
PP_TUPLE_INSERT(1, 2, (1, 3)) // -> (1, 2, 3)
PP_TUPLE_POP_BACK(()) // -> ()
PP_TUPLE_POP_BACK((1)) // -> ()
PP_TUPLE_POP_BACK((1, 2, 3)) // -> (1, 2)
PP_TUPLE_REMOVE(1, (1, 2, 3)) // -> (1, 3)
PP_TUPLE_REMOVE(0, (1, 2, 3)) // -> (2, 3)
Let me briefly explain the implementation of inserting elements here. Similar principles are also used to implement other operations such as deleting elements. PP_TUPLE_INSERT(i, elem, tuple)
can insert element elem
at position i
in tuple
. To complete this operation, we first use PP_TUPLE_PUSH_BACK
to put all elements smaller than position i
onto a new tuple
called ret
. Then we place element elem
at position i
, and later we place all the elements in the original tuple
that are larger than or equal to position i
after ret
. Finally, ret
will be the desired result.
Summary
(https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms#deferred-expression), the relevant macros inside BOOST_PP REPEAT
and so on, those who are interested can search for information on their own.
Debugging macro programming is a painful process, and there are several things we can do to make it easier:
Use the -P -E
options to output the preprocessing results.
Examine the expanding process in detail using the modified clang
version mentioned earlier.
* Break down complex macros and examine the expansion results of intermediate macros;
* Exclude unnecessary header files and macros;
* Finally, there is the process of mentally simulating macro expansion. Once familiar with macro expansion, the efficiency of debugging will also improve.
The macros mentioned in this text are my own implementation after understanding the principles. Some of the macros are inspired by the implementation and references in Boost articles. If there are any mistakes, please feel free to point them out at any time. I am also open to discussing related issues with you.
All the code for this document is available here: Download,Online Demo.
> Quote
Original: https://wiki.disenone.site/en
This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.
Visitors. Total Visits. Page Visits.
This post is translated using ChatGPT, please feedback if any omissions.