Introduction to Turing completeness of c++ pretreatment

  • 2020-05-26 09:51:52
  • OfStack

Let's talk about c++. In recent years, playing code generation in c++, preprocessing is an inevitable and indispensable tool. Although the boost pp preprocessor library is very perfect in the use of macros, there is also too much code, and the code is very difficult to understand, which inevitably makes people wonder, is it necessary to make so much complex, do so much code? Moreover, after looking at the boostpp interface, I feel that it is not clean and easy to combine. Therefore, a new set of pretreated wheels was made. The following code is supposed to run on msvc2013 +, since many people use MSVC's own wheels, which tend to support msvc first.

First, let's define a macro to turn the input into a string. Gee, this is too easy, but, here, I feel like I need to explain this one more time. The following code convention is that all available macro functions are capitalized starting with PP, and all macro functions starting with _ZPP are internal implementations, which can be made a bit uglier. Because macro functions are global, have no concept of scope, and are simply text substitutions, when they die, they do not know how to die, so they must be treated with caution. Just like the windows.h header file, min and max are the names of the macros. Although they are very convenient to use, they don't know how much trouble they cause. So, in many cases, when windows.h is included, the first thing is undef min and max.

The following code, can be arbitrary in a project, you can create a source file of cpp suffix name, and then press CTRL+F7 to compile, do not need F5, you can see the effect of the operation, if the compilation passed, it means that the macro is basically correct, the more test code, the higher the accuracy. Of course, you can also set the properties of the source file and have msvc generate the pre-processed file, then open that file with notepad and watch it.


#define PP_TEXT(str) _ZPP_TEXT(str)
#define _ZPP_TEXT(str) #str

In the c++ preprocessor macro, the operator # is the expression followed by two double quotation marks, or strings. PP_TEXT(str) is not directly defined as #str, but by calling _ZPP_TEXT(str), and then turning the input into a string there, which is a little bit complicated, a little bit too much. However, it is actually to support macro expansion in all direction, that is, when the input str itself also has macro calls, it is totally unhelpful. For example, if you implement this


#define PP_TEXT(str) #str

So, for the following case,


#define AAA aaa

PP_TEXT(AAA), the result will be "AAA" instead of "aaa". Because the macro operator directly turns the input into a string, there is no room for 1 bit of wiggle room in the input, so an indirection layer has to be introduced to give the input a chance to expand the macro. Later, many macro functions are implemented in such a way that they have to be called indirectly in order for the macro to fully expand. The macro expansion mechanism of msvc is more bizarre, less human, and its indirect invocation is uglier. There is no way to do this.
And then, in order to debug the macro, or to test the macro, of course, a lot of times, to debug the macro, you still have to open the preprocessed file for comparison. We make 1 dot packaging for static_assert, because static_assert needs two parameters. In the c++ version after c++11, it seems that static_assert only needs one parameter, so this packaging is not needed at that time.


#define PP_ASSERT() static_assert((__VA_ARGS__), PP_TEXT(__VA_ARGS__));

PP_ASSERT (...). The three inside are macro s with variable parameters, while s = s = s = s = s = s = s = s = s = s = s = s = s = s = s = s = s = s = s = s All the parameters that are matched, this syntax is very important to be familiar with. Here, instead of explaining its use in detail, there will be a large number of macro functions using s 71en_es 72en__.
Ok, we can start with PP_ASSERT(...) We did the test.
PP_ASSERT(2+3==5)
If, then, you compile this file and you find that it's compiled, for example
PP_ASSERT(2+3==4)
At compile time, an error message, error C 2338:2 +3==4, is reported
Well, with the test setup in place, you can start writing code with impunity. Step by step, build the Turing completeness of the c preprocessor macro.
Obviously, the most important thing is to connect two macro parameters together, which is the ## operator. Obviously, just like the # operation, you have to give the inside parameter a chance to expand the macro, so you have to call it indirectly. Here is the implementation


#define PP_JOIN(_A, _B) _ZPP_JOIN_I(_A, _B)
#define _ZPP_JOIN_I(_A, _B) _ZPP_JOIN_II(~, _A##_B)
#define _ZPP_JOIN_II(p, res) res

It turns out that there are not only one layer of indirection, but two layers of indirection, and one more layer of indirection, because I found that when I was doing macro recursion, I could not fully expand the macro by calling the first layer of indirection, so I had to add another layer of indirection. I don't know why, and I am too lazy to investigate. Now, the next step, of course, is to test PP_JOIN. Everyone, you can create a new test file, the file include our macro function. Of course, you can also write the test code in the same file, note that divided into two sections of code, the first section to write macro function, the next section to write the test code, so far, can be, and later.


PP_ASSERT(PP_JOIN(1+2, == 3))
#define A 20
#define B 10
PP_ASSERT(PP_JOIN(A + B, == 30))

With PP_JOIN, you can start doing something else. For example, a counter,


#define _ZPP_INC_JOIN(_A, _B) _ZPP_INC_JOIN_IMP1(_A, _B)
#define _ZPP_INC_JOIN_IMP1(_A, _B) _ZPP_INC_JOIN_IMP2(~, _A##_B)
#define _ZPP_INC_JOIN_IMP2(p, res) res

#define PP_INC(x, ) _ZPP_INC_JOIN(_ZPP_INC_, x)
#define _ZPP_INC_0 1
#define _ZPP_INC_1 2
#define _ZPP_INC_2 3
#define _ZPP_INC_3 4
#define _ZPP_INC_4 5
#define _ZPP_INC_5 6
#define _ZPP_INC_6 7
#define _ZPP_INC_7 8
#define _ZPP_INC_8 9
#define _ZPP_INC_9 10

Here, we have re-implemented PP_JOIN once again, which can't be helped. Later, when the PP_JOIN is heavily nested, PP_JOIN will be included in PP_JOIN, which will cause the macro to stop expanding, so we have to use our own version of JOIN for every place where JOIN is needed.
This is the implementation of the macro function, through and then text replacement, 11 enumeration, to achieve this effect, that is, we through the JOIN function, in the macro to construct a counter data type. Wouldn't it be tiring to write every macro function like this? The good news is that by implementing a few basic functions in this way, and then passing through the macro's recursion engine, the other macro functions don't have to be replaced one by one.
PP_ASSERT(PP_INC(9)==10)
PP_ASSERT(PP_INC(PP_INC(9)) == 11)
When you get used to writing test code, it's fun to write it, and the test passes, which is the most exciting moment.
Next, deal with the gross behavior of the macros in msvc, and then finish the introduction.


#define PAIR_SECOND(x, y) y
PP_ASSERT(PAIR_SECOND(10, 20) == 20)

That's not bad. Now, define1 macro function returns 1 pair, which is 2 values

#define MAKE_PAIR(x, y) x, y

And then, when I call,
PAIR_SECOND(MAKE_PAIR(10, 20))
The compiler was immediately unhappy that the warning C4003: "PAIR_SECOND" macro didn't have enough arguments
It seems that instead of expanding MAKE_PAIR(10, 20) and then calling PAIR_SECOND, the compiler simply passes MAKE_PAIR(10, 20) as a function to PAIR_SECOND, and then PAIR_SECOND prompts for insufficient arguments.
PP_ASSERT(PAIR_SECOND(MAKE_PAIR(10, 20)) == 20)
Obviously, the compiler would be furious anyway. For this, we have to introduce another layer of indirection and find a way to make MAKE_PAIR(10, 20) expand first and then pass it to PAIR_SECOND. So, you can't just use the form, PAIR_SECOND(MAKE_PAIR(10, 20)). Had to be changed to this, the following a few lines of code, very a little bit shocking world make the gods cry taste.


#define _ZPP_INVOKE_JOIN(_A, _B) _ZPP_IMP_INVOKE_JOIN_I(_A, _B)
#define _ZPP_IMP_INVOKE_JOIN_I(_A, _B) _ZPP_IMP_INVOKE_JOIN_II(~, _A##_B)
#define _ZPP_IMP_INVOKE_JOIN_II(p, res) res

#define PP_INVOKE(m, args, ) _ZPP_INVOKE_JOIN(m, args) 

The first few lines of code are PP_INVOKE JOIN function implementation, you can directly as they are JOIN functions, the key is PP_INVOKE(m, args...) Here, the first parameter m is the macro function, the second parameter args is the list of parameters to be passed to the first parameter m, enclosed in parentheses, and the ellipses are sometimes added to please the compiler, I don't know why, but that's all. Garbage macro, garbage preprocessing, as long as you can complete the function, c++, code generation code, tmp, the focus of the macro is just a small necessary auxiliary tools. And then, when I call,
PP_ASSERT(PP_INVOKE(PAIR_SECOND, (MAKE_PAIR(10, 20))) == 20)
Compile through, very not easy!


Related articles: