3 States of Preprocessor Macros

Mon
30
Jun 2025

This will be a beginner-level article for programmers working in C, C++, or other languages that use a similar preprocessor - such as shader languages like HLSL or GLSL. The preprocessor is a powerful feature. While it can be misused in ways that make code more complex and error-prone, it can also be a valuable tool for building programs and libraries that work across multiple platforms and external environments.

In this post, I’ll focus specifically on conditional compilation using the #if and #ifdef directives. These allow you to include or exclude parts of your code at compile time, which is much more powerful than a typical runtime if() condition. For example, you can completely remove a piece of code that might not even compile in certain configurations. This is especially useful when targeting specific platforms, external libraries, or particular versions of them.

When it comes to enabling or disabling a feature in your code, there are generally two common approaches:

Solution 1: Define or don’t define a macro and use #ifdef:

// To disable the feature: leave the macro undefined.

// To enable the feature: define the macro (with or without a value).
#define M

// Later in the code...

#ifdef M
    // Use the feature...
#else
    // Use fallback path...
#endif

Solution 2: Define a macro with a numeric value (0 or 1), and use #if:

// To disable the feature: define the macro as 0.
#define M 0
// To enable the feature: define the macro as a non-zero value.
#define M 1

// Later in the code...

#if M
    // Use the feature...
#else
    // Use fallback path...
#endif

There are more possibilities to consider, so let’s summarize how different macro definitions behave with #ifdef and #if in the table below:

  #ifdef M #if M
(Undefined) No No
#define M Yes ERROR
#define M 0 Yes No
#define M 1 Yes Yes
#define M (1) Yes Yes
#define M FOO Yes No
#define M "FOO" Yes ERROR

The #ifdef M directive simply checks whether the macro M is defined, no matter if it has empty value or any other value. On the other hand, #if M attempts to evaluate the value of M as an integer constant expression. This means it works correctly if M is defined as a literal number like 1 or even as an arithmetic expression like (OTHER_MACRO + 1). Interestingly, using an undefined symbol in #if evaluates to 0, but defining a macro with an empty value or a non-numeric token (like a string) will cause a compilation error - such as “error C1017: invalid integer constant expression” in Visual Studio.

It's also worth noting that #if can be used to check whether a macro is defined by writing #if defined(M). While this is more verbose than #ifdef M, it’s also more flexible and robust. It allows you to combine multiple conditions using logical operators like && and ||, enabling more complex preprocessor logic. It is also the only option when doing #elif defined(OTHER_M), unless you are using C++23, which adds missing #elifdef and #elifndef directives.

So, which of the two approaches should you choose? We may argue about the one or the other, but when developing Vulkan Memory Allocator and D3D12 Memory Allocator libraries, I decided to treat some configuration macros as having three distinct states:

  1. The user explicitly defined the macro as 0, meaning they want the feature disabled.
  2. The user explicitly defined the macro as 1, meaning they want the feature enabled.
  3. The user left the macro undefined, meaning they have no preference - so I make the decision based on internal logic.

To support this pattern, I use the following structure:

#ifndef M
    #if (MY OWN CONDITION...)
        #define M 1
    #else
        #define M 0
    #endif
#endif

// Somewhere later...

#if M
    // Use the feature...
#else
    // Use fallback path...
#endif

Comments | #c++ Share

Comments

[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2025