How can I detect from the preprocessor what a macro’s definition is?


It is common that a preprocessor macro chooses between multiple behaviors based on various other controlling macros.

#ifdef BUILD_DLL
#define CONTOSOAPI __declspec(dllexport)
#else
#define CONTOSOAPI __declspec(dllimport)
#endif

#ifdef USE_STDCALL
#define CONTOSOAPICALL __stdcall
#else
#define CONTOSOAPICALL __cdecl
#endif

Suppose you want to check at compile time how these macros are defined. Is there a way to do string comparisons in the preprocessor, something like this?

#if somethingsomething CONTOSOAPI == __declspec(dllexport)

I'll get it out of the way up front: Instead of trying to parse the value of a macro, you can just replicate the conditionals that led to the macro's definition. In other words, you can do this:

#ifdef BUILD_DLL
... stuff to do when CONTOSOAPI is __declspec(dllexport)
#else
... stuff to do when CONTOSOAPI is __declspec(dllimport)
#endif

#ifdef USE_STDCALL
... stuff to do when CONTOSOAPICALL is __stdcall
#else
... stuff to do when CONTOSOAPICALL is __cdecl
#endif

But let's say that this option isn't available. For example, maybe the logic that eventually leads to the definition of CONTOSOAPI is super-complicated and difficult to replicate. Or the header file is not under your control and you want your code to adapt to newer versions of the header file that may use different logic to decide what definition to use.

The C and C++ preprocessors do not do string comparisons. All they can do is evaluate constant integral expressions. So things don't sound good.

But wait, maybe we can trick them into evaluating constant integral expressions!

#define __declspec
#define dllexport 1
#define dllimport 2

#if CONTOSOAPI == __declspec(dllexport)
... stuff to do when CONTOSOAPI is __declspec(dllexport)
#elif CONTOSOAPI == __declspec(dllimport)
... stuff to do when CONTOSOAPI is __declspec(dllimport)
#else
#error I don't know what CONTOSOAPI is defined as
#endif

#undef dllimport
#undef dllexport
#undef __declspec

#define __stdcall 1
#define __cdecl 2

#if CONTOSOAPICALL == __stdcall
... stuff to do when CONTOSOAPICALL is __stdcall
#elif CONTOSOAPICALL == __cdecl
... stuff to do when CONTOSOAPICALL is __cdecl
#else
#error I don't know what CONTOSOAPICALL is defined as
#endif

#undef __cdecl
#undef __stdcall

By redefining the words that appear in the CONTOSOAPI and CONTOSOAPICALL macros, you can turn the text into integer constant expression. After macro expansion, __declspec(dllexport) becomes (1), and __declspec(dllimport) becomes (2). These are integer constant expressions that can be evaluated by the preprocessor!

Why did I choose 1 and 2 as the integer constants rather than 0 and 1? One of the rules of the C and C++ preprocessors is that after macro substitution, if there are any identifiers remaining whose values are not known, then they are treated as zero. This means that __declspec(magicbeans) expands to (magicbeans), and since there is no definition for magicbeans, the preprocessor treats it as zero. If I had defined dllexport as 0, then I would misdetect __declspec(magicbeans) as dllexport.

This is extreme abuse of the C and C++ preprocessor. But desperate times may call for desperate measures.

Bonus chatter: Note that this trick requires that you find some way of defining symbols so that what remains is a valid integer constant expression.

Comments (18)
  1. Cesar says:

    The traditional answer to this question is “add an autoconf test for it” (autoconf can compile and run arbitrary test programs at configure time). Of course, that’s not always an option.

    1. Okay, so how do you write a test program to determine how the macro is defined?

      1. D Taylor 84 says:

        #define STR(x) #x

        if (!strcmp(STR(CONTOSOAPI), “__declspec(dllexport)”)
        exit(0);

        exit(1);

        1. D Taylor 84 says:

          Well, I can’t work out how to format that legibly, but basically “stringification”.

        2. True, though it breaks if whitespace changes. Probably not an issue in practice.

          1. R P (MSFT) says:

            If it were an issue in practice, you could use std::regex instead of strcmp.

          2. Joshua says:

            Again, it just boils down to how much code you’re willing to write.

            The thing that annoys me is in modern C++ you’re not allowed to redefine keywords anymore. Thankfully the compiler vendors have their act together we’ll enough to provide an option to turn that dumb rule off.

  2. Quantum says:

    I think he meant using # to turn it into a string and print it.

  3. Thomas Harte says:

    Sometimes I wonder what proportion of these things you’ve stumbled upon as a result of your role in maintaining backwards compatibility. Surely having to come up with integral substitutions that definitely produce unique results does just that little amount less than would be ideal to ensure compatibility with future changes to headers beyond your control? It feels intuitively like the sort of maintenance issue that, ten years and three developers later, might result in a call to Microsoft for help under the pretext of “this was working, now it isn’t, ergo you broke this”.

  4. SwineOne says:

    Well wouldn’t you know it, just this week I had the exact same problem, and eventually I solved it exactly that way (save for the fact I didn’t know about the issue with 0, so I’ll go change my program now).

  5. mikeb says:

    Clever, but please let me never have to deal with code that uses this technique.

  6. James Touton says:

    It’s worth noting here that defining a macro with the same name as a language keyword is forbidden, and the preprocessor is allowed to reject that.

  7. Jon Cohen says:

    This is solvable in C++ as opposed to using autoconf or cmake:

    https://godbolt.org/g/d6ZCq2

    That is in c++17 for ease of implementation, but it’s possible in C++11 using recursion instead of a for loop.

  8. Jon Cohen says:

    You can solve this in C++: https://godbolt.org/g/d6ZCq2

    This implementation is in C++17 but it should work in c++11 as well, just with a slightly uglier look since you have to use recursion instead of iteration in the constexpr string comp

    1. Provided the behavior you want to vary is expressible within the C++ language. But what if you wanted to #include a different header file depending on the setting? Or declare a member variable conditionally? (You can sort of fake the latter with a templatized struct, but it’s not as neat as an #ifdef.)

  9. Paul Sanders says:

    Ingenious, Raymond, ingenious!

  10. Peter Doubleday says:

    And why would you ever feel the desire to do that?

  11. Chris says:

    One thing I’ve seen is an accompanying macro with an integral value that is defined alongside the actual behaviour you want. Checking which behaviour is in use boils down to checking the accompanying macro.

Comments are closed.

Skip to main content