Saturday, June 2, 2012

C++ Macros aren't completely evil, I promise

As a C++ programmer you'll undoubtedly hear of many macro horror stories. Macros can be easily abused, can bloat your program, and can be a debugging nightmare. But, macros are not completely evil, when used properly they can be clean and useful.

The other day I was updating a logging system. For those that don't know, a logging system (or logger) is usually a system created along side a program or engine that allows programmers to log messages, or errors, and to stream their output in different ways, like to a debug console, or to a file, or even an email in some rare cases (like a critical program failing in a bad way and a live product team needs to know immediately). Anyway, I was updating a logging system, and I needed two things that it did not yet offer:
  • I needed the parameters being logged to have no expense within the shipped product (the product that was in the customer's hands). 
    • Without macros there are ways people try to limit logging commands:
      • Multiple function definitions for your logging commands, the shipped version of these do nothing when called.
        #define SHIPPING_BUILD
        #include <iostream>
        
        


namespace logging


        {
        

#ifndef SHIPPING_BUILD
            void print( int i )


            {


                std::cout << i;


            }
        

#else


            void print ( int i ) { }
        

#endif
        

}
        
        


int expensiveFcnReturningAnInt()


        {
            // The compiler would actually optimize this so it would


            // be extremely cheap to call, but you get the idea, this


            // function would be something you don't want to happen


            // in a shipping build if used only for logging.

 
            return 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1;


        }
        
        


int main(int argc, const char * argv[])


        {


            logging::print( expensiveFcnReturningAnInt() );


        
            return 0;


        }
      • In the above example the expense of the logging itself is removed, there is nothing logged to the debug console, or written to a log file. But the main issue here, is that expensiveFcnReturningAnInt() was still called, which is expensive, completely unnecessary, and useless
    • With macros you can affect the code at compile time. In this example the logging statement will no longer even exist in the program for shipping builds.
      • #define SHIPPING_BUILD
        #include <iostream>
        
        #ifndef SHIPPING_BUILD
            #define INTERNAL_PRINT(x) logging::print(x)
        #else
            #define INTERNAL_PRINT(x) ((void)0)
        #endif
        
        namespace logging
        {
            void print( int i )
            {
                std::cout << i;
            }
        }
        
        int expensiveFcnReturningAnInt()
        {
            // The compiler would actually optimize this so it would
            // be extremely cheap to call, but you get the idea, this
            // function would be something you don't want to happen
            // in a shipping build if used only for logging.
            return 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1;
        }
        
        int main(int argc, const char * argv[])
        {
            INTERNAL_PRINT( expensiveFcnReturningAnInt() );
            
            return 0;
        }
        
    • In this sample, the call inside of main() to INTERNAL_PRINT is completely optimized out of the program when SHIPPING_BUILD is defined.
  • I needed a simple, one-line logging command that accepted a stream of information, just like an std::stringstream.
    • Without a macro, you would be forced to make an entire class to try and get you close to this functionality, and even then you might not get it. 
    • With a macro you can have exactly what you want:
    • #define SHIPPING_BUILD
      #include <iostream>
      
      #ifndef SHIPPING_BUILD
      #include <sstream>
      
      #define PRINT_STREAM(x) \
          { \
              std::stringstream strStream; \
              strStream << x; \
              logging::print(strStream.str().c_str()); \
          } 
      #else         
          #define PRINT_STREAM(x) ((void)0) 
      #endif
      
      namespace logging
      {
          void print( const char* message )
          {
              std::cout << message;
          }
      }
      
      int expensiveFcnReturningAnInt()
      {
          // The compiler would actually optimize this so it would
          // be extremely cheap to call, but you get the idea, this
          // function would be something you don't want to happen
          // in a shipping build if used only for logging.
          return 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1;
      }
      
      int main(int argc, const char * argv[])
      {
          PRINT_STREAM( "Expensive fcn result is: " << expensiveFcnReturningAnInt() );
          
          return 0;
      }
I would recommend splitting your logging stuff into its own header, or class even. The examples above have everything in the same file/snippet to keep things easier to read for this blog.

No comments:

Post a Comment