foonathan::blog()

Thoughts from a C++ library developer.

The year is 2017 - Is the preprocessor still needed in C++?

The C++, eh C, preprocessor is wonderful.

Well, no - it isn’t wonderful.

It is a primitive text replacement tool that must be used to work with C++. But is “must” really true? Most of the usage has become obsolete thanks to new and better C++ language features. And many more features like modules will come soon™. So can we get rid of the preprocessor? And if so, how can we do it?


Advertisement

Much of the preprocessor use is already bad practice: Don’t use it for symbolic constants, don’t use it for inline functions etc.

As this gained a lot of traction, let me clarify something: I’m not advocating for removing the preprocessor with this post. Some people in the C++ community and many on the standardization committee want to do that, however, so I wanted to explore the feasibility.

But there still a few ways it is used in idiomatic C++. Let’s go through them and see what alternative we have.

Header File Inclusion

Let’s start with the most common usage: #include a header file.

Why is the preprocessor needed?

In order to compile a source file, the compiler needs to see the declarations of all functions that are being called. So if you define a function in one file, and want to call it in another, you have to declare it in that file as well. Only then can the compiler generate the appropriate code to call the function.

Of course, manually copying the declaration can lead to errors: If you change the signature you have to change all declarations as well. So, instead of manually copying the declarations, you write them in a special file - the header file, and let the preprocessor copy it for you with #include. Now you still need to update all declarations, but just in one place.

But plain text inclusion is dumb. It can sometimes happens that the same file gets included twice, which leads to two copies of that file. This is no problem for function declarations, but if you have class definitions in an header file, that’s an error.

To prevent that, you have to use include guards or the non-standard #pragma once.

How can we replace it?

With current C++ features, we can’t (without resorting to copy pasta).

But with the Modules TS we can. Instead of providing header files and source files, we can write a module and import that.

If you want to learn more about modules, I highly recommend the most recent CppChat.

Conditional Compilation

The second most common job of the preprocessor is conditional compilation: Change the definitions/declarations by defining or not defining a macro.

Why is the preprocessor needed?

Consider the situation where you’re writing a library that provides a function draw_triangle() which draws a single triangle on the screen.

Now the declaration is straightforward:

// draws a single triangle
void draw_triangle();

But the implementation of the function changes depending on your operating system, window manager, display manager and/or moon phase (for exotic window manager).

So you need something like this:

// use this one for Windows
void draw_triangle()
{
    // create window using the WinAPI 
    // draw triangle using DirectX
}

// use this one for Linux
void draw_triangle()
{
    // create window using X11
    // draw triangle using OpenGL
}

The preprocessor helps there:

#if _WIN32
    // Windows triangle drawing code here 
#else
    // Linux triangle drawing code here
#endif

The code in the branch that is not taken will be deleted before compilation, so we won’t get any errors about missing APIs etc.

How can we replace it?

C++17 adds if constexpr, this can be used to replace simple #if … #else:

Instead of this:

void do_sth()
{
    #if DEBUG_MODE
        log();
    #endif
    
}

We can write this:

void do_sth()
{
    if constexpr (DEBUG_MODE)
    {
        log();
    }

    
}

If DEBUG_MODE is false, then the branch will not be compiled properly, it will only check for syntax errors, similar to the checking done for a not yet instantiated template.

This is not correct, as was pointed out. It will still be fully checked if outside a template. It doesn’t matter here though, as it still doesn’t have any runtime overhead.

This is even better than #if as it will spot obvious errors in the code without checking all macro combinations. Another benefit with if constexpr is that DEBUG_MODE can now be a normal constexpr variable, instead of a constant coming from a macro expansion.

And if you don’t have if constexpr, you can use class template specializations, or tag dispatching.

Of course, there are downsides to if constexpr: You can’t use it to constrain preprocessor directives, i.e. #include. For the draw_triangle() example, the code needs to include the proper system header. if constexpr can help, so you’d need true conditional compilation there or manually copy the declarations.

This is better than normally as system header declarations are usually pretty stable. But it’s still not recommended.

And modules can’t help either as the system headers do not define any module you can import. Furthermore, you can’t conditionally import a module (as far as I know).

Passing configuration options

On a related note, you sometimes want to pass some configuration options to a library. You might want to enable or disable assertions, precondition checks, change some default behavior…

For example, it might have a header like this:

#ifndef USE_ASSERTIONS
    // default to enable
    #define USE_ASSERTIONS 1
#endif

#ifndef DEFAULT_FOO_IMPLEMENTATION
    // use the general implementation
    #define DEFAULT_FOO_IMPLEMENTATION general_foo
#endif


When building the library you can then override the macros either when invoking the compiler, or through CMake, for example.

How can we replace it?

Macros are the obvious choice here, but there are is an alternative:

We could use a different strategy to pass options, like policy-based design, where you pass a policy to a class template that defines the chosen behavior. This has the benefit that it doesn’t force a single implementation to all users, but of course has its own downsides.

But what I’d really like to see is the ability to pass these configuration options when you import the module:

import my.module(use_assertions = false);

This would be the ideal replacement for:

#define USE_ASSERTIONS 0
#include "my_library.hpp"

But I don’t think that’s technically feasible without sacrificing the benefits modules provide, i.e. pre-compiling modules.

Assertion macros

The macro you’ll most commonly use probably does some kind of assertion. And macros are the obvious choice here:

  • You’ll need to conditionally disable assertions and remove them so they have zero overhead in release.
  • If you have a macro, you can use the pre-defined __LINE__, __FILE__ and __func__ to get the location where the assertion is and use that in the diagnostic.
  • If you have a macro, you can also stringify the expression that is being checked and use it in the diagnostic as well.

That’s why almost all assertions are macros.

How can we replace it?

I’ve already explored how conditional compilation can be replaced and how you can specify whether or not they should be enabled, so that’s no problem.

Using policy-based design here also allows customization of how the diagnostic is reported to the user.

Getting the file information is also possible in the Library Fundamentals TS v2 as it adds std::experimental::source_location:

void my_assertion(bool expr, std::experimental::source_location loc = std::experimental::source_location::current())
{
    if (!expr)
        report_error(loc.file_name, loc.line, loc.function_name);
}

The function std::experimental::source_location::current() expands to the information about the source file at the point of writing it. Furthermore, if you use it as a default argument, it will expand to the caller location. So the second point is no problem either.

The third point is the critical one: You can’t stringify the expression and print it in the diagnostic without using a macro. If you’re okay with that, you can implement your assertion function today.

But otherwise you still need a macro for that. Check out this blog post how you could implement an (almost) macro-less assertion function, where you can control the level with constexpr variables instead of macros. You can find the full implementation here.

Compatibility macros

Not all compilers support all C++ features, which makes porting a real pain, especially if you don’t have access to a compiler for a testing and need to do the “change a line, push to CI, wait for CI build, change another line” cycle just because some compiler really doesn’t like an important C++ feature!

Anyways, the usual compatibility problems can be solved with macros. The implementations even define certain macros once they’ve implemented a feature, making checking trivial:

#if __cpp_noexcept
    #define NOEXCEPT noexcept
    #define NOEXCEPT_COND(Cond) noexcept(Cond)
    #define NOEXCEPT_OP(Expr) noexcept(Expr)
#else
    #define NOEXCEPT
    #define NOEXCEPT_COND(Cond)
    #define NOEXCEPT_OP(Expr) false
#endif



void func() NOEXCEPT
{
    
}

This allows a portable usage of features even though not all compilers have them already.

How can we replace it?

We can’t do that in any other way. Workaround missing features requires some kind of preprocessing tool to get rid of not-supported features. We have to use macros here.

Boilerplate macros

C++’s templates and TMP go a long way to eliminate a lot of boilerplate code you otherwise need to write. But sometimes, you just need to write a lot of code that is the same but not quite the same:

struct less
{
    bool operator()(const foo& a, const foo& b)
    {
        return a.bar < b.bar;
    }
};

struct greater
{
    bool operator()(const foo& a, const foo& b)
    {
        return a.bar > b.bar;
    }
};


Macros can generate that boilerplate for you:

#define MAKE_COMP(Name, Op) \
struct Name \
{ \
    bool operator()(const foo& a, const foo& b) \
    { \
        return a.bar Op b.bar; \
    } \
};

MAKE_COMP(less, <)
MAKE_COMP(greater, >)
MAKE_COMP(less_equal, <=)
MAKE_COMP(greater_equal, >=)

#undef MAKE_COMP

This can really save you a lot of repetitive code.

Or consider the case where you need to workaround ugly SFINAE code:

#define REQUIRES(Trait) \
    typename std::enable_if<Trait::value, int>::type = 0

template <typename T, REQUIRES(std::is_integral<T>)>
void foo() {}

Or you need to generate the to_string() implementation for an enum, it’s a simple task with X macros:

// in enum_members.hpp
X(foo)
X(bar)
X(baz)

// in header.hpp
enum class my_enum
{
    // expand enum names as-is
    #define X(x) x,
    #include "enum_members.hpp"
    #undef X
};

const char* to_string(my_enum e)
{
    switch (e)
    {
        // generate case
        #define X(x) \
            case my_enum::x: \
                return #x;
        #include "enum_members.hpp"
        #undef X
    };
};

They just make a lot of code easier to read and work with: You don’t need copy-paste, you don’t need fancy tools and there is no real “danger” for the user.

How can we replace it?

We can’t replace all of those with a single language feature. For the first one, we need a way to pass an overloaded function (like an operator) to a template, then we could pass it as template parameter and simply alias it. For the second one, we need concepts. And for the third one, we need reflection.

So there is no way to get rid of such boilerplate macros without resorting to manually writing the boilerplate code.

Conclusion

With current C++(17), most of the preprocessor use can’t be replaced easily.

The Modules TS allows a replacement of the most common usage - #include, but still the preprocessor is sometimes necessary, especially to ensure platform and compiler compatibility.

And even then: I think that proper macros, which are part of the compiler and very powerful tools for AST generation, are a useful thing to have. Something like Herb Sutter’s metaclasses, for example. However, I definitely don’t want the primitive text replacement of #define.


Advertisement