Five examples for avoiding the preprocessor

The C++ preprocessor is a text replacement tool used to transform the source code in order to produce a single text file that is then passed to the actual compiler. It has various capabilities, such as including files, conditional compilation, text macro replacement, error emitting, stringizing, or token concatenation. Often developers use the preprocessor when other alternatives are available and are more appropriate. In this article, I will show five examples of when and how you can avoid the use of the preprocessor.

Table of contents:

  1. Object-like macros
  2. Function-like macros
  3. Exception handling macros
  4. Conditional compilation
  5. Policy-like macros
  6. Bonus: including files

Object-like macros

These are identifiers that are replaced with a fragment of code and are often used to give symbolic names to numerical or string literals. Here is a typical example you must have seen many times.

Instead of being a macro, BUFFER_SIZE could, and should, be defined as a compile-time constant.

Notice it is declared as constexpr and not just const. The latter indicates a value that does not change, but might only be available at runtime. The former, implies constness, but is guaranteed to be available at compile-time. constexpr values can be used in any place where compile-time constants are expected.

Many times object-like macros are used to define related symbolic names, such as in the following example:

Following the previous example, we can simply replace these with constexpr values (in a class or namespace scope):

However, these macros, representing bitflags here, can also be replaced with an enumerator.

This code is more verbose than the original one and you might be tempted to avoid writing all these explicit casts. You can actually make it as simple as the original and avoid macros, by overloading various operators for the enumerator type. The following snippet shows the completely rewritten example.

Function-like macros

These are macros that look like functions. The macro name is followed by none, one, or more arguments in paranthesis. Most of the times these can be replaced with regular functions or function templates. Let us get back to the previous example with the permission bigflag macros and introduce a couple function-like macros for setting and testing bit flag values.

The SETBIT and TESTBIT macros can be replaced with inline functions (SETBIT being replaced by two functions, one that sets a bit and one that resets a bit). For the following example, I assume the permissions scoped enum and the overloaded operators are defined as above.

Exception handling macros

Confronted with repeated situations when they have to catch the same exeptions and handle them the same way, some developers resort to macros for avoiding repetitive code. The following is such an example.

If you execute this program it will print runtime error: an error has occurred!. However, these macros are not debugable, and in practice may even be hard to write. This example can be rewritten to use a regular function as handler for multiple exceptions. The only difference in the main() function is an additional call for this function, error_handler().

The throw; statement without an expression rethrows the currently handled exception. (It is allowed only when an exception is being handled, otherwise std::terminate() will be called). It can be used to create handlers that can catch and handle multiple exception types without the need to duplicate code or resort to macros.

Conditional compilation

This is a feature of the preprocessor that selects whether to include or not a chunk of code in the final text file that will be passed to the compiler. Preprocessor conditional directives can check arithmetic expressions or whether a name is defined as a macro.

In the following example, a message is written to the standard output stream when the program is compiled using a debug configuration and the _DEBUG macro is defined.

In C++17 this can be replaced with constexpr if as shown in the following example:

Policy-like macros

OK, that’s not a term you see in the literature, but I couldn’t find something better, and that looks the closest to what we have here. Let’s look at the following example and exaplain what we’re doing.

The goal is to trace the execution of functions. We want a message to be displayed to the console when the function starts and another one when the function stops. The first message should show the function name and the current time, and the end message must show the function name, the current time and duration of the function execution. The class Tracer defines a conversion constructor, that prints a message to the console, and records a start time point, and a custom destructor, that computes the time since the constructor was called and prints another message to the console. Defining objects of this type at the beginning of a function will have the result that a message is printed after the function execution started and another one just before it ends. However, we only want to do that in some cases, when a particular macro name (called MONITORING in this example) is defined. This can be defined either in code, or passed as an argument to the compiler (like -DMONITORING). This goal can be achieved using macros, as in the following example:

If you run this program having MONITORING defined, the output looks like the following:

Should MONITORING not be defined, the output is simply

Using constexpr if is not possible in this situation, because that would introduce an inner scope. In other words, the following example:

would result in the following code being generated

As a result, the Tracer object would be created and immediatelly destroyed at the beginning of the function.

A solution for this problem is to use policy-based design. We can define policies, i.e. classes, that perform or do not perform any tracing. The foo() function would become a function template, parameterized with the monitoring policy. Then, we can use std::conditional to select between policies at compile time based on a condition. That condition would be the availability of the MONITORING macro name. This can be passed as a compiler argument, or else it will be defined as 0 in the code. Here is how the example could look in this case:

We are still left with two macros: MONITORING to select one policy or another, and __FUNCTION__ to get the undecorated name of the enclosing function. There is no way to replace the former for the time being, but for the latter, there is something under review in library fundamentals Technical Specification v2, called std::experimental::source_location. This will provide information about the source code, such as the line number and enclosing function name. Using this special built-in class, we would be able to get rid of the __FUNCTION__ special macro as following:

Bonus: including files

Including files is definitely the most common preprocessor functionality. Is there an alternative to it? Not yet, but one is in work. It’s called modules and a technical specification is in work. It is likely that a first version will be available in C++20. Basically, the #include directive for headers, will be replaced with import directives for modules. Here is a very simple example:

With modules available, this can be changed as follows:

7 Replies to “Five examples for avoiding the preprocessor”

  1. I just hate static_cast, and then extra layer of operators is unnecessary… I ended up with the following instead:

    namespace permissions
    enum : int
    none = 0,
    read = 1,
    write = 2,
    add = 4,
    del = 8

    void show_permissions(int const p)
    if(p & permissions::read)
    std::cout << "can read" << std::endl;
    if (p & permissions::write)
    std::cout << "can write" << std::endl;
    if (p & permissions::add)
    std::cout << "can add" << std::endl;
    if (p & permissions::del)
    std::cout << "can delete" << std::endl;

    int main()
    auto flags = permissions::read | permissions::write;


    flags |= permissions::del | permissions::add;
    flags &= permissions::write;

    std::cout << permissions::write << std::endl;

  2. Nice article. But the example of the TRACE macro in the “Conditional compilation” is not very strong. The typical definition of a TRACE macro would allow:

    #ifdef NDEBUG
    #define TRACE(x)
    #define TRACE(x) debug_printf(x);

    void function_foo()
    TRACE(“In function_foo.”);

    the if constexpr construction requires 2 lines, which is more error prone.

  3. Thanks for the comment. My intention was to show how we can replace conditional checks like #ifdef SOMETHING with constexpr if. Maybe the example I picked was not the most fortunate. Maybe it could have been something without another macro involved, several statements or something maybe harder to be simplified the way you did. But I believe I touched on the example you are mentioning in the other paragraph that I called policy-like macros.

  4. #include

    inline void trace(std::string_view text)
    std::cout << text << std::endl;

    int main()
    if constexpr(_DEBUG)
    trace("debug build");

    This doesn't compile when _DEBUG is not defined (when -D_DEBUG is not passed to the compiler)

    clang++ -std=c++17 conditional.cpp -o conditional -stdlib=libc++
    conditional.cpp:11:16: error: use of undeclared identifier
    if constexpr(_DEBUG)

    But, adding these lines allows compilation with and without -D_DEBUG and gives the desired effect.
    #ifndef _DEBUG
    #define _DEBUG 0

    I am not sure if this the best way to fix it, but just that it works.

  5. Another way to avoid conditional preprocessor based inline compilation, which can get messy, is to place platform or target specific code in separate but identically named source files in platform or target specific subdirs and use environment or build vars as input to the build system to determine which source files are compiled and linked. Along these lines, build specific code can be integrated into the class structure using virtual methods or subclassing.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.