Dirty C ++ Mac Tricks

    In this article I want to do two things: to tell why macros are evil and how to deal with it, as well as to demonstrate a couple of C ++ macros that I use, which simplify the work with the code and improve its readability. The tricks, in fact, are not so dirty:
    • Safe method call
    • Unused variables
    • Conversion to string
    • The comma in the macro argument
    • Endless cycle

    I warn you in advance: if you are thinking of seeing something cool, puzzling and stunning under the cut, then there is nothing like that in the article. An article about the bright side of macros.

    Some useful links


    For starters: Anders Lindgren's article , Tips and tricks using the preprocessor (part one) , covers the very basics of macros.
    Advanced: An Anders Lindgren - Tips and tricks article using the preprocessor (part two) covers more serious topics. Something will be in this article, but not all, and with fewer explanations.
    For professionals: an article (in English) by Aditya Kumar, Andrew Sutton, Bjarne Stroustrup - Rejuvenating C ++ Programs through Demacrofication , describes the possibilities for replacing macros with C ++ 11 features.

    Slight cultural difference


    According to Wikipedia and my own feelings, in Russian we usually mean by the word "macro" this:
    #define FUNC(x, y) ((x)^(y))
    
    And the following:
    #define VALUE 1
    
    we call it “preprocessor constant” (or simply “define” 'ohm). In English, it’s a little different: the first is called function-like macro, and the second is called object-like macro (again, here is a link to Wikipedia ). That is, when they talk about macros, they can mean both one and the other, and all together. Be careful when reading English texts.

    What is good and what is bad


    Recently, it has been popularly believed that macros are evil . This opinion is not groundless, but, in my opinion, needs clarification. In one answer to the question Why are preprocessor macros evil and what are the alternatives? I found a fairly complete list of reasons that make us consider macros evil and some ways to get rid of them. Below I will give the same list in Russian, but the examples and solutions to the problems will not be exactly the same as on the specified link.
    1. Macros cannot be debugged
      Firstly, in fact, you can:
      Go to either project or source file properties by right-clicking and going to "Properties". Under Configuration Properties-> C / C ++ -> Preprocessor, set "Generate Preprocessed File" to either with or without line numbers, whichever you prefer. This will show what your macro expands to in context. If you need to debug it on live compiled code, just cut and paste that, and put it in place of your macro while debugging.
      So, it would be more correct to say that "macros are difficult to debug." But, nevertheless, a problem with debugging macros exists.

      To determine if the macro you are using needs debugging, think about whether there is something for which you want to push a breakpoint there. This can be a change in the values ​​obtained through parameters, declaration of variables, change of objects or data from the outside, and the like.
      Solutions to the problem:
      • completely get rid of macros by replacing them with functions (you can inline if it's important),
      • transfer the logic of macros into functions, and make the macros themselves responsible only for transferring data to these functions,
      • Use only macros that do not require debugging.
    2. When you expand the macro, strange side effects may appear.
      To show which side effects are involved, they usually give an example with arithmetic operations. I will not depart from this tradition either:
      #include 
      #define SUM(a, b) a + b
      int main()
      {
          // Что будет в x?
          int x = SUM(2, 2);
          std::cout << x << std::endl;
          x = 3 * SUM(2, 2);
          std::cout << x << std::endl;
          return 0;
      }
      
      In the output, we expect 4 and 12, but we get 4 and 8. The fact is that the macro simply substitutes the code where it is indicated. And in this case, the code will look like this:
      int x = 3 * 2 + 2;
      
      This is a side effect. To make it work, as expected, we need to change our macro:
      #include 
      #define SUM(a, b) (a + b)
      int main()
      {
          // Что будет в x?
          int x = SUM(2, 2);
          std::cout << x << std::endl;
          x = 3 * SUM(2, 2);
          std::cout << x << std::endl;
          return 0;
      }
      
      Right now. But that is not all. Let's move on to multiplication:
      #define MULT(a, b) a * b
      
      We immediately write it “correctly”, but use it a little differently:
      #include 
      #define MULT(a, b) (a * b)
      int main()
      {
          // Что будет в x?
          int x = MULT(2, 2);
          std::cout << x << std::endl;
          x = MULT(3, 2 + 2);
          std::cout << x << std::endl;
          return 0;
      }
      
      Deja vu: again we get 4 and 8. In this case, the expanded macro will look like:
      int x = (3 * 2 + 2);
      
      That is, now we need to write:
      #define MULT(a, b) ((a) * (b))
      
      We use this version of the macro and voila:
      #include 
      #define MULT(a, b) ((a) * (b))
      int main()
      {
          // Что будет в x?
          int x = MULT(2, 2);
          std::cout << x << std::endl;
          x = MULT(3, 2 + 2);
          std::cout << x << std::endl;
          return 0;
      }
      
      Now everything is correct.

      If we ignore arithmetic operations, then, in the general case, when writing macros, we need
      • brackets around the whole expression
      • brackets around each macro parameter
      That is, instead of
      #define CHOOSE(ifC, chooseA, otherwiseB) ifC ? chooseA : otherwiseB
      
      should be
      #define CHOOSE(ifC, chooseA, otherwiseB) ((ifC) ? (chooseA) : (otherwiseB))
      

      This problem is compounded by the fact that not all types of parameters can be wrapped in brackets (a real example will be further in the article). Because of this, making high-quality macros can be quite difficult.

      In addition, as the encyclopedist recalled in the comments, there are times when the brackets do not save:
      In the section about side effects, you still forgot to mention a common problem - macros can calculate their arguments several times. In the worst case, this leads to strange side effects, in the milder one - to performance problems.
      Example

      #define SQR(x) ((x) * (x))
      y = SQR(x++);
      
      Solutions to the problem:
      • abandon macros in favor of functions,
      • use macros with a friendly name, simple implementation and well-placed brackets so that a programmer using such a macro can easily understand how to use it correctly.
    3. Macros do not have a namespace
      If a macro is declared, it is not only global, but also simply will not allow you to use something with the same name (the macro implementation will always be substituted). The most famous example is probably the problem with min and max under Windows .
      The solution is to choose names for macros that are less likely to intersect with something, for example:
      • names in UPPERCASE, usually they can only intersect with other macro names,
      • names with a prefix (the name of your project, namespace, something else unique), intersection with other names will be possible with very little probability, but it will be a little more difficult for people to use such macros outside of your project.
    4. Macros can do something you don't suspect
      Actually, this is the problem of choosing a name for the macro. Let's say we take the same example that is given in the answer by the link:
      #define begin() x = 0
      #define end() x = 17
      ... a few thousand lines of stuff here ... 
      void dostuff()
      {
          int x = 7;
          begin();
          ... more code using x ... 
          printf("x=%d\n", x);
          end();
      }
      
      There are clearly chosen names, which are misleading. If the macros were called set0toX () and set17toX () or something like that, problems could be avoided.
      Solutions to the problem:
      • competently name macros,
      • replace macros with functions,
      • Do not use macros that implicitly change anything.

    After all of the above, you can define “good” macros. Good macros are macros that
    • do not require debugging (inside there is simply no need to set a breakpoint)
    • have no side effects when deployed (everything is wrapped in brackets)
    • do not conflict with names elsewhere (this type of names is selected that are unlikely to be used by anyone else)
    • do not change anything implicitly (the name accurately reflects what the macro does, and all work with the surrounding code, if possible, is carried out only through the parameters and the "return value")

    Safe method call


    The old version, not tested by Habré
    #define prefix_safeCall(value, object, method) ((object) ? ((object)->method) : (value))
    #define prefix_safeCallVoid(object, method) ((object) ? ((void)((object)->method)) : ((void)(0)))
    

    In fact, I used this version
    #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments) ((objectPointer) ? ((objectPointer)->methodWithArguments) : (defaultValue))
    #define prefix_safeCallVoid(objectPointer, methodWithArguments) ((objectPointer) ? static_cast((objectPointer)->methodWithArguments) : static_cast(0))
    
    But Habr is not an IDE, so such long lines look ugly (at least on my monitor), and I reduced them to a readable look.

    tenzink in the comments pointed out a problem with these macros , which I safely did not take into account when writing the article:
    prefix_safeCallVoid(getObject(), method());
    
    With this call, getObject is called twice.

    Unfortunately, as the article showed, not every programmer will guess this, so I can no longer consider these macros to be good. :-(

    Nevertheless, I met similar macros (somewhat differently implemented) in real production code, they were used by a team of programmers, including me. There were no problems because of them in my memory

    The new version, which appeared thanks to lemelisk and C ++ 14:
    #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\
    [&](auto&& ptr) -> decltype(auto)\
    {\
        return ptr ? (ptr->methodWithArguments) : (defaultValue);\
    }\
    (objectPointer)
    #define prefix_safeCallVoid(objectPointer, methodWithArguments)\
    [&](auto&& ptr)\
    {\
        if(ptr)\
            (ptr->methodWithArguments); \
    }\
    (objectPointer)

    Version for C ++ 11
    #define prefix_safeCallBaseExpression(defaultValue, objectPointer, methodWithArguments)\
    ((ptr) ? ((ptr)->methodWithArguments) : (defaultValue))
    #define prefix_safeCall(defaultValue, objectPointer, methodWithArguments)\
    [&](decltype((objectPointer))&& ptr)\
        -> decltype(prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments))\
    {\
        return prefix_safeCallBaseExpression(defaultValue, ptr, methodWithArguments);\
    }\
    (objectPointer)
    #define prefix_safeCallVoid(objectPointer, methodWithArguments)\
    [&](decltype((objectPointer))&& ptr)\
    {\
        if (ptr)\
            (ptr->methodWithArguments);\
    }\
    (objectPointer)

    Pay attention to the methodWithArguments parameter. This is the same example of a parameter that cannot be wrapped in brackets. This means that in addition to calling the method, you can also push something into the parameter. However, accidentally arranging this is quite problematic, so I do not consider these macros to be "bad."

    In addition, now we have added overhead to the lambda call. Theoretically, it can be assumed that the lambda, called where it is defined, will be inline. But I did not find confirmation of this on the network, so it would be best to check it "manually" for your compiler.

    How these two macros are used, I think, is understandable. If we have a code:
    auto somePointer = ...;
    if(somePointer)
        somePoiter->callSomeMethod();
    
    then using the safeCallVoid macro it turns into:
    auto somePointer = ...;
    prefix_safeCallVoid(somePointer, callSomeMethod());
    
    and, similarly, for the case with the return value:
    auto somePointer = ...;
    auto x = prefix_safeCall(0, somePointer, callSomeMethod());
    

    For what? First of all, these macros allow you to increase the readability of the code, reduce nesting. The greatest positive effect is given in conjunction with small methods (that is, if you follow the principles of refactoring).

    Unused variables


    #define prefix_unused(variable) ((void)variable)
    

    In fact, the option I use is also different
    #define prefix_unused1(variable1) static_cast(variable1)
    #define prefix_unused2(variable1, variable2) static_cast(variable1), static_cast(variable2)
    #define prefix_unused3(variable1, variable2, variable3) static_cast(variable1), static_cast(variable2), static_cast(variable3)
    #define prefix_unused4(variable1, variable2, variable3, variable4) static_cast(variable1), static_cast(variable2), static_cast(variable3), static_cast(variable4)
    #define prefix_unused5(variable1, variable2, variable3, variable4, variable5) static_cast(variable1), static_cast(variable2), static_cast(variable3), static_cast(variable4), static_cast(variable5)
    Please note that starting with two parameters, this macro could theoretically have side effects. For greater reliability, you can use the classics:
    #define unused2(variable1, variable2)  do {static_cast(variable1); static_cast(variable2);} while(false)
    
    But, in this form it is more difficult to read, which is why I use the less "safe" option.

    A similar macro is, for example, in cocos2d-x, where it is called CC_UNUSED_PARAM. Among the shortcomings: theoretically, it may not work on all compilers. However, in cocos2d-x it is defined exactly the same for all platforms.

    Using:
    int main()
    {
        int a = 0; // неиспользуемая переменная.
        prefix_unused(a);
        return 0;
    }
    

    For what? This macro avoids the warning about an unused variable, and he reads the code, as it were: “the one who wrote this knew that the variable was not used, everything was in order.”

    Conversion to string


    #define prefix_stringify(something) std::string(#something)
    

    Yes, it’s so harsh, right away in std :: string. The pros and cons of using the string class will be left out of the conversation; we’ll only talk about the macro.

    You can use it like this:
    std::cout << prefix_stringify("string\n") << std::endl;
    
    And so:
    std::cout << prefix_stringify(std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
    
    And even so:
    std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)
    std::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
    
    However, in the last example, the line break will be replaced with a space. For real transfer you need to use '\ n':
    std::cout << prefix_stringify(#define prefix_stringify(something) std::string(#something)\nstd::cout << prefix_stringify("string\n") << std::endl;) << std::endl;
    
    You can also use other characters, for example, '\' to concatenate strings, '\ t', and others.

    For what? It can be used to simplify the output of debugging information or, for example, to create a factory of objects with text id (in this case, such a macro can be used when registering a class in the factory to turn the class name into a string).

    The comma in the macro parameter


    #define prefix_singleArgument(...) __VA_ARGS__
    

    The idea is spied here .

    An example from there:
    #define FOO(type, name) type name
    FOO(prefix_singleArgument(std::map), map_var);
    

    For what? It is used if necessary to transfer an argument containing commas to another macro as one argument and the inability to use parentheses for this.

    Endless cycle


    #define forever() for(;;)
    

    Version by Joel Spolsky
    #define ever (;;)
    for ever { 
       ...
    }
    
    PS If someone, following the link, did not think to read the name of the question, then it sounds like “what was the worst real abuse of macros?” ;-)

    Using:
    int main()
    {
        bool keyPressed = false;
        forever()
        {
            ...
            if(keyPressed)
                break;
        }
        return 0;
    }
    

    For what? When while (true), while (1), for (;;) and other standard ways of creating a loop do not seem very informative, you can use a similar macro. The only plus he gives is a slightly better readability of the code.

    Conclusion


    When used properly, macros are not at all bad. The main thing is not to abuse them and follow simple rules for creating "good" macros. And then they will become your best helpers.

    Upd . Returned to the article “Safe Method Call”, thanks lemelisk for the help with lambdas.

    PS
    And what interesting macros do you use in your projects? Feel free to share in the comments.

    Only registered users can participate in the survey. Please come in.

    Are you still on the bright side of C ++, or have you already taken the dark path?

    • 28% I am a Jedi! I do not use macros at all! 259
    • 37.5% I'm on the bright side, I use only “good” macros; or “bad,” but proven. 346
    • 13.6% I am on the dark side and use a variety of macros. 126
    • 4.1% I'm on the dark side! Is there anything else besides macros in C ++? 38
    • 16.5% I am Emperor Palpatine! 153

    Also popular now: