Wednesday, October 1, 2008

C++ Coding Standard: Preprocessor Issues

One of the strengths that C++ inherited from C is the preprocessor. Text macros and conditional compilation provide expressive power that can greatly simplify certain programming tasks. On the other hand, that expressive power can also lead to obfuscated code -- or worse, undebuggable code.

So our C++ coding standard guides you gently away from preprocessor abuse.
  • 9.1. Preprocessor macros should be rarely used.
    • 9.1.1. Obvious exception to 9.1: multiple-include guard.
    • 9.1.2. Use enum constants instead of constant macros.
    • 9.1.3. Use inline functions instead of function macros (when possible).
  • 9.2. Never use a macro to invent new syntax.
  • 9.3. Conditional compilation should be rarely used.
  • 9.4. Operating system #includes use angle brackets, e.g. <unistd.h>.
  • 9.5. Non-operating system #includes use double quotes, e.g. "Object.hpp".
  • 9.6. Use the standard C++ system header files, e.g. <string> not <string.h>.
Let's get the last three items out of the way first, since they're the easiest. The issue is that compilers do a slightly different search for header files depending on the form of the #include. So choose the correct form.

Section 9.1 suggests alternatives to preprocessor macros. What's the difference? For constants, the differences are that the enum name is visible from a debugger, and an accidental re-definition of a constant becomes a compile-time error, instead of -- if you're lucky -- a warning. For functions, again, you get visibility in the debugger if you use an inline function, but not if you use a function macro. Function macros can also be harder for humans to read, and you have to remember to put every instance of a macro argument into parenteses in the macro definition, to ensure that the text expansion has the same semantics as the macro reference.

When should you use preprocessor macros? One place that I find them useful is in specifying error messages. It would be handy to keep the message text together with the message name, and indeed to have a message enum in the program that also gets printed in the output. I'll describe the details of this in a future post, but you can accomplish the goal above by having a single macro call that puts the name of the message together with its text, but that accomplishes the different tasks by defining the macro differently.

Section 9.3 discourages conditional compilation. Almost any project that has to be compiled on more than one platform or compiler will require some conditional compilation. The key here is to isolate as much of it as possible into a single header file. That way most of your project remains readable, but each platform gets supported correctly.

Next: everyone's favorite topic, naming conventions.

No comments: