When you're holding a hammer, everything looks like a nail. It's tempting once you know how
#ifdef works to use it as a sort of means to obtaining custom behavior in your program. I know because I made the same mistake.
I inherited a legacy program written in MFC C++ which already used
#ifdef to define platform-specific values. This meant I could compile my program to be used on a 32-bit platform or a 64-bit platform simply by defining (or in some cases not defining) specific macro values.
The problem then arose that I needed to write custom behavior for a client. I could have created a branch and made a separate code base for the client, but that would have made a maintenance hell. I could have also defined configuration values to be read by the program on startup and used these values to determine behavior, but I would then have to create custom setups to add the proper configuration values to the configuration files for each client.
I was tempted, and I gave in. I wrote
#ifdef sections in my code to differentiate the various behavior. Make no mistake, it was nothing over the top at first. Very minor behavior changes were made which allowed me to redistribute versions of the program to our clients and I need not have more than one version of the code base.
Over time this became maintenance hell anyway because program no longer behaved consistently across the board. If I wanted to test a version of the program, I had to necessarily know who the client was. The code, though I tried to reduce it to one or two header files, was very cluttered and the quick fix approach that
#ifdef provided meant such solutions spread throughout the program like a malignant cancer.
I've since learned my lesson, and you should too. Use it if you absolutely must, and use it strictly for platform changes. The best way to approach behavior differences between programs (and therefore clients) is to change only the configuration loaded on startup. The program remains consistent and it both becomes easier to read as well as to debug.