You only offer emotional arguments ("a blight", "relic from the past", etc.).
Yes, the preprocessor does not work at the same level as the compiler - and that is the good thing about it because it gives you leverage about what the compiler sees and it allows you to guarantee that the logic outside the #ifdefs is untouched by any changes - therefore you get much higher quality/stability.
Did you just say logic? Have you ever written complex sets of code with the preprocessor? A heap of nested macros? This thing scales very badly, and is not even turing complete. C++ templates also scale badly, that's a given (complex metacode is a horror, just like complex macros calling other macros), but they have knowledge about typing information and semantics of the language. By that I mean things like namespaces. Template metaprograms also run in a different realm (at compile-time, and not at run-time), so where does that leave the preprocessor?
In case you missed it: I said that in C, the preprocessor needs to be used a lot more often. I do not argue against that (I know it firsthand from writing C code for embedded hardware). I do argue that *in C++*, the preprocessor does not have to be used nearly as often. There is *zero* reason for a MIN() macro when you can have a templated min() function, for example.
Your example with the untested feature can be solved by isolating the crazy untested code in its own module, and simply *not enabling that module in the build scripts*.
So you have to have modules for every tiny feature?
And all that bloat and overhead just to satisfy your emotional sense of aesthetics?
So to avoid 2 lines of "ugly" code (#ifdef / #endif) you need to create a module, adapt the build-system, etc. etc.?
And we have not even gone into some "advanced" stuff like
#if defined(TEST_1) && defined(TEST_2)
So easy to do with the preprocessor - how do you do that with modules? Create a third module that contains just the code that is needed when both other modules are included? And hide everything in the build-system so that nobody can find and/or debug it?
And again, why all that overhead when all you get is a program that is slower, uses more RAM and (yes!) is much more difficult to understand and debug?
Ideally, the buld-system should not contain any logic. All the logic should be in the source-code.
And of course your "aesthetics before function" - approach may be acceptable on the PC where all that bloat does not matter much. But it is a absolute no-go in embedded-systems programming. Just two years ago I have worked in a project where we had only 128 KB (yes, that is kilobytes) of RAM. And we had to frequently cut the bloat to stay under that limit.
In that situation you forget about "modules", object-orientation and all that other buzz-words from the ivory-tower pretty fast.
If you seriously believe that modularization and object oriented programming are stuff that has no practical usage, then you obviously do not know much about them. Here's a hint: these things are incredibly useful and can even be applied to tiny platforms like stuff that you program with Keil compilers. Yes, things with 32K SRAM or less, no full standard C library (usually hardly any library at all), no heap, etc. I have worked on these. I have applied modularization and object orientation to them. No, it wasn't bloated. No, object oriented programming does not imply huge amounts of registries, virtual function tables, or deep class hierarchies. It is all about having a proper architecture where separate concerns are handled by separate modules. Not one big piece of magical code doing it all, in a messy, convoluted way. The fact that you call such essential concepts "aesthetics" and "ivory tower stuff" speaks volumes.
And I obviously do not advocate imperative logic in the build scripts. It is trivial to see that I mean different configurations for different feature sets if the changes are big and it makes sense to do it this way (for example, some additional profiling and analysis utility functions in a Debug configuration). If it is small stuff you are talking about, for example some experimental changes to the code, then I suggest you make use of version control and set up a separate branch. I also have done this for embedded projects ranging from tiny CSR chipsets with 8K SRAM and small-scale Cypress hardware to bigger ones like Cortex-A9 based hardware. I *never* put #ifdefs for new experimental stuff in the code, I use git branches instead. Both the workflow and the code quality are vastly superior as a result.
I do put an #if 0 block a few times if a certain feature is known to be problematic with the current BSP and/or kernel, together with a very detailed comment that explains the issue, and to please enable this once the faulty dependency is fixed. The conditional compilation feature is something that other languages have adapted, without adding all of the preprocessor. And hopefully, C++ will have static_if one day, so #if 0 can finally go. (Unlike #if 0, static_if can access metacode, and would make template metaprogramming vastly easier to read and program.)
Not to mention the combinatorial explosion with your TEST_1 / TEST_2 macros another poster already pointed at.
So what do you do when you have a new revision of a circuit board that has a different pin-layout?
Do you throw away everything (several man-months of programming and testing) and create a sophisticated module-system that will create numerous other problems and limitations to satisfy aesthetics?
No: You use the preprocessor to add the new stuff while still avoiding any change for the old, so the old stuff can still be used and tested and (more importantly) you can compare the old with the new.
I put the pin layout data in separate .c files (one file for each layout), and link the one that is appropriate for the board, and keep one header around, which contains a forward declaration of that table. No #ifdef necessary. And this is not a new or complex idea. It has been around since the dawn of C. And no, this is not complicated to set up in a Makefile.