Why using if statements in preprocessor defines? - if-statement

When exploring a source package, I discovered that some if statements were defined by preprocessor directives and then used in the code like the example below:
#ifdef _WIN32
#define OSisWindows() true
#define OSisLinux() false
#elif defined(__linux__)
#define OSisLinux() true
#define OSisWindows() false
#endif
if ( OSisWindows() )
{
// do something
}
if ( OSisLinux() )
{
// do something else
}
Is there a difference between using this and simple defines like _WIN32 or __linux__ ?

Well, you can't use a macro directly in your code like:
if ( _WIN32 ) {
...
}
on linux for example because it isn't defined so they are simply mapping the macro to a boolean expression.
In in end, it's really a style decision though... they could have certainly done:
#ifdef _WIN32
...
#endif

Assuming the macros are defined, the advantage of using an if..else statement over an #if .. #else directive is all code paths are compiled in the first case so if there are compile errors the compiler is able to see them in all the code paths. Of course this isn't always possible as some code paths may be platform dependent code.

In addition to what's already been written it's another layer of abstraction. If for example the syntax for _WIN32 changes or you want to explicitly compile for windows without relying on the flag, you'd have to change your complete code in case you would have used _WIN32 directly. In this case you could just change the preprocessor directives.

Is there a difference between using this and simple defines like _WIN32 or linux ?
No, this is simple macro abuse.

Related

What does #define do if you only have an identifer

typically #define would be used to define a constant or a macro. However it is valid code to use #define in the following way.
#define MAX // does this do anything?
#define MAX 10 // I know how to treat this.
So, if I #define MAX 10, I know my pre-processor replaces all instances of MAX with 10. If someone uses #define MAX by itself however with no following replacement value, it's valid. Does this actually DO anything?
My reason for asking is that I am writing a compiler for c in c++ and handling preprocessor directives is required but I haven't been able to find out if there is any functionality I need to have when this occurs or if I just ignore this once my preprocess is done.
My first instinct is that this will create a symbol in my symbol table with no value named MAX, but it is equally possible it will do nothing.
As an add in question which is kind of bad form I know, but I'm really curious. Are there situations in real code where something like this would be used?
Thanks,
Binx
A typical example are header guards:
#ifndef MYHEADER
#define MYHEADER
...
#endif
You can test if something is defined with #ifdef / ifndef.
It creates a symbol with a blank definition, which can later be used in other preprocessor operations. There are a few things it can be used for:
1) Branching.
Consider the following:
#define ARBITRARY_SYMBOL
// ...
#ifdef ARBITRARY_SYMBOL
someCode();
#else /* ARBITRARY_SYMBOL */
someOtherCode();
#endif /* ARBITRARY_SYMBOL */
The existence of a symbol can be used to branch, selectively choosing the proper code for the situation. A good use of this is handling platform-specific equivalent code:
#if defined(_WIN32) || defined(_WIN64)
windowsCode();
#elif defined(__unix__)
unixCode();
#endif /* platform branching */
This can also be used to dummy code out, based on the situation. For example, if you want to have a function that only exists while debugging, you might have something like this:
#ifdef DEBUG
return_type function(parameter_list) {
function_body;
}
#endif /* DEBUG */
1A) Header guards.
Building on the above, header guards are a means of dummying out an entire header if it's already included in a project that spans multiple source files.
#ifndef HEADER_GUARD
#define HEADER_GUARD
// Header...
#endif /* HEADER_GUARD */
2) Dummying out a symbol.
You can also use defines with blank definitions to dummy out a symbol, when combined with branching. Consider the following:
#ifdef _WIN32
#define STDCALL __stdcall
#define CDECL __cdecl
// etc.
#elif defined(__unix__)
#define STDCALL
#define CDECL
#endif /* platform-specific */
// ...
void CDECL cdeclFunc(int, int, char, const std::string&, bool);
// Compiles as void __cdecl cdeclFunc(/* args */) on Windows.
// Compiles as void cdeclFunc(/* args */) on *nix.
Doing something like this allows you to write platform-independent code, but with the ability to specify the calling convention on Windows platforms. [Note that the header windef.h does this, defining CDECL, PASCAL, and WINAPI as blank symbols on platforms that don't support them.] This can also be used in other situations, whenever you need a preprocessor symbol to only expand to something else under certain conditions.
3) Documentation.
Blank macros can also be used to document code, since the preprocessor can strip them out. Microsoft is fond of this approach, using it in windef.h for the IN and OUT symbols often seen in Windows function prototypes.
There are likely other uses as well, but those are the only ones I can think of off the top of my head.
It doesn't "do" anything in the sense that it will not add anything to a line of code
#define MAX
int x = 1 + 2; MAX // here MAX does nothing
but what an empty define does is allow you to conditionally do certain things like
#ifdef DEBUG
// do thing
#endif
Similarly header guards use the existance of a macro to indicate if a file has already been included in a translation unit or not.
The C Preprocessor (CPP) creates a definitions table for all variables defined with the #define macro. As the CPP passes through the code, it does at least two things with this information.
First, it does a token replacement for the defined macro.
#define MAX(a,b) (a > b) ? (a) : (b)
MAX(1,2); // becomes (1 > 2) ? (1) : (2);
Second, it allows for those definitions to be searched for with other preprocessor macros such as #ifdef, #ifndef, #undef, or CPP extensions like #if defined(MACRO_NAME).
This allows for flexibility in using macro definitions in those cases when the value is not important, but the fact that a token is defined is important.
This allows for code like the following:
// DEBUG is never defined, so this code would
// get excluded when it reaches the compiler.
#ifdef DEBUG
// ... debug printing statements
#endif
#define does a character-for-character replacement. If you give no value, then the identifier is replaced by...nothing. Now this may seem strange. We often use this just to create an identifier whose existence can be checked with #ifdef or #ifndef. The most common use is in what are called "inclusion guards".
In your own preprocessor implementation, I see no reason to treat this as a special case. The behavior is the same as any other #define statement:
Add a symbol/value pair to the symbol table.
Whenever there is an occurrence of the symbol, replace it with its value.
Most likely, step 2 will never occur for a symbol with no value. However, if it does, the symbol is simply removed since its value is empty.

Force the compiler to ignore some lines in the program

Suppose that I have 10,000 lines of C++ code. 200 lines of this code are for testing purpose (for example, check the program and show an error message).
Is there an way in C++ to ignore or consider some lines of the code (maybe with preprocessor keywords)?
Short answer:
Use macros and #ifdef checking. For example:
#ifdef MY_CONTROL_MACRO
...
#endif
the code within this scope will only be compiled if you already defined the MY_CONTROL_MACRO macro.
More stuff:
To define such a macro, you can
Add #define MY_CONTROL_MACRO to your code. Or,
For VS, add MY_CONTROL_MACRO to Project > Properties > C/C++ > Preprocessor > Preprocessor Definitions. Or,
For GCC, compile your code with option -DMY_CONTROL_MACRO.
You can check out here for more info.
This block is called a conditional group. controlled text will be included in the output of the preprocessor if and only if MACRO is defined. We say that the conditional succeeds if MACRO is defined, fails if it is not.
The controlled text inside of a conditional can include preprocessing directives. They are executed only if the conditional succeeds. You can nest conditional groups inside other conditional groups, but they must be completely nested. In other words, ‘#endif’ always matches the nearest ‘#ifdef’ (or ‘#ifndef’, or ‘#if’). Also, you cannot start a conditional group in one file and end it in another.
You can also use the advanced ifdef-else-endif style:
#ifdef MY_CONTROL_MACRO
... // this part will be valid if MY_CONTROL_MACRO is defined
#else
... // this part will be valid if MY_CONTROL_MACRO is NOT defined
#endif
Surround the code with "#ifdef...#endif", and then use the compiler options to set the flag:
#ifdef MYTEST_ONLY_FUNCTIONALITY_ENABLED
...
#endif
You can then use the compiler options to include this code. For example, in GCC:
-DMYTEST_ONLY_FUNCTIONALITY_ENABLED
Though, to be honest, I think this approach is generally not very maintainable in large projects and, if possible, it is generally better to simply move the test-only code to a completely separate library (without this conditional logic) and simply link that code into your test binary rather than your non-test binary. That also avoids having to compile each of the other libraries in both debug and non-debug modes.
This is what #ifdef was designed for
You put
#ifdef TESTS
... test code ...
#endif
and then you can pass to the compiler options to decide if you want the test part compiled in or not. For example with g++ it's
g++ -DTESTS ...
Using a preprocessor guard is definitely the most flexible and common approach. However, when possible, I suggest using an if statement. For example, instead of
void example(int a){
int some_local;
...
#ifdef _DEBUG
std::cout << "In function " << __FUNCTION__ << "(" << a <<")" << std::endl;
#endif
....
}
Assuming ENABLE_DEBUG is defined to be 0 or non-zero, I would use
void example(int a){
int some_local;
...
if(ENABLE_DEBUG) std::cout << "In function " << __FUNCTION__ << "(" << a <<")" << std::endl;
...
}
Since ENABLE_DEBUG is a constant, when ENABLE_DEBUG is 0 the compiler will not generate any code for statements it guards. So, why use this method instead of #ifdef?
If there are many separate debug statements spread throughout the code, it can be a bit easier to read
More importantly, the code is always processed for syntactic errors, even if no code is generated. This can be very helpful if the debug code is not frequently enabled. If variables change (e.g. in the above example if the argument a was renamed), then the person making the change will know they have to update the debug statement as well. If #ifdefs are used, then it can hide bit rot until someone needs to enable the debug code and then they have to go and try and fix up the code, something that may not be obvious to them.
Obviously this approach only works for debug statements inside method/function bodies.
Go with the existing convention, and use the NDEBUG macro. All common compilers define this macro for release builds, and do not define it for debug builds.
The macro originally existed to control the output of assert(3), and is defined as such all the way back in the POSIX standard and at least since C89.
Note that you have to reverse the test with #ifndef.
An example:
#ifndef NDEBUG
/* Debugging code */
std::cerr << "So far we have seen " << unicorns << " unicorns" << std::endl;
#endif
P.S. With gcc/g++, you do a debug build by adding -g to the command line.
Surround your testing code #ifdef DEBUG.
#if DEBUG
....
#endif
The way to go is using preprocessor directive with the define passed to the compiler or taken from a header "config.h":
#if defined(DEBUG) // or #ifdef DEBUG
// Debug code
#endif
To avoid to use everywhere in source code:
#if defined(DEBUG)
My_Debug_function(some_variable)
#endif
You may do in the header
#if !defined(DEBUG) // or #ifndef DEBUG
# define My_Debug_function(some_variable) do { static_cast<void>(some_variable); } while (false) /* Do nothing */
#endif
And so use My_Debug_function almost normally.
Use preprocessor #define and #if
depending on your compiler, you should have some variables available by default i.e NDEBUG (for not-debug) or DEBUG
you can define a variable yourself in code by
#define MY_VARIABLE
and use it as follows
#ifdef MY_VARIABLE
//code that compiles only if MY_VARIABLE is defined
printf("test output here");
#else
//code that compiles only if MY_VARIABLE is NOT defined
printf("MY_VARIABLE is not defined");
#endif
for more information search online for
#define, #if, #ifdef, #ifndef

C/C++ #define Macro inside macro?

I would like something like:
#define C_OR_CPP(C__, CPP__) #ifdef __cplusplus\
CPP__\
#else\
C__\
#endif
Is it possible?
Maybe some dirty hack with #include ?
Reason:
I make a header where a struct uses a member variable of type vector<stuff>*, but in C i want it to simply be void*, you know.
TIA
What's the problem with
#ifdef __cplusplus
#define C_OR_CPP(C, CPP) CPP
#else
#define C_OR_CPP(C, CPP) C
#endif
(Leaving names with double underscore to the implementation per phresnel remark)
My English is poor, and I'm sorry for language mistakes and typos if any.
If #ifdef must not wrap the macro invocation, there is a solution not so graceful.
g++ only:
You may try this in selective occasions. But if there are commas in a or b, workarounds are still needed.
It's simply based on the fact that __cplusplus is defined to "1" when in a C++ environment and remains itself while not.
#define SELECT1(a, b) a
#define SELECT__cplusplus(a, b) b
#define xcat(a,b) a##b
#define concat(...) xcat(__VA_ARGS__)
#define C_OR_CPP(C, CPP) concat(SELECT, __cplusplus)(C, CPP)
C_OR_CPP(1, 2)
Other Environments
Check the __cplusplus macro, a compiler that comforming to standard C++ should generate
#define __cplusplus value
and value should >= 199711L
Not in C++. But you can
#ifdef __cplusplus
# define CPP
#else
# define C
#endif
I assume this is just a pathological example by you. Note also that double underscore is reserved to library implementors (see 17.6.4.3.2 Global names).
vector, but in C i want it to simply be void, you know.
So, what speaks against a solution like
struct Foo {
#ifdef __cplusplus
...
#else
...
#endif
};
or what speaks against providing different APIs for different programming languages?
AProgrammer already given you the right answer, but the answer to the "is it possible" part of the question is no. Macro expansion doesn't occur until after all preprocessor directives have been handled, so any macro that expands into a #define or #ifdef will be passed to the compiler as regular source text, which will cause the compiler to yak.

Can I use #undef this way?

I want to get some settings I store in the registry, and if they differ from a #define I want to redefine it, could I do it this way?:
#define DEFINED_X "testSetting"
void LoadConfig()
{
regConfigX = some value previusly stored in the registry;
if(regConfigX!=DEFINED_X)
{
#undef DEFINED_X
#define DEFINED_X regConfigX
}
}
I tought #define was used only when compiling, would this code work when running the compiled exe?
No. #define and #undef are preprocessing directives; they are evaluated before the source code is compiled.
You need to use a variable for this, not a macro.
#define and #undef occur before your source code even hits the compiler. Anything to do with #defines can't happen at runtime.
You should check out the Boost preprocessor library, too.
No, use a static variable to store the value of DEFINED_X.

#ifdef vs #if - which is better/safer as a method for enabling/disabling compilation of particular sections of code?

This may be a matter of style, but there's a bit of a divide in our dev team and I wondered if anyone else had any ideas on the matter...
Basically, we have some debug print statements which we turn off during normal development. Personally I prefer to do the following:
//---- SomeSourceFile.cpp ----
#define DEBUG_ENABLED (0)
...
SomeFunction()
{
int someVariable = 5;
#if(DEBUG_ENABLED)
printf("Debugging: someVariable == %d", someVariable);
#endif
}
Some of the team prefer the following though:
// #define DEBUG_ENABLED
...
SomeFunction()
{
int someVariable = 5;
#ifdef DEBUG_ENABLED
printf("Debugging: someVariable == %d", someVariable);
#endif
}
...which of those methods sounds better to you and why? My feeling is that the first is safer because there is always something defined and there's no danger it could destroy other defines elsewhere.
My initial reaction was #ifdef, of course, but I think #if actually has some significant advantages for this - here's why:
First, you can use DEBUG_ENABLED in preprocessor and compiled tests. Example - Often, I want longer timeouts when debug is enabled, so using #if, I can write this
DoSomethingSlowWithTimeout(DEBUG_ENABLED? 5000 : 1000);
... instead of ...
#ifdef DEBUG_MODE
DoSomethingSlowWithTimeout(5000);
#else
DoSomethingSlowWithTimeout(1000);
#endif
Second, you're in a better position if you want to migrate from a #define to a global constant. #defines are usually frowned on by most C++ programmers.
And, Third, you say you've a divide in your team. My guess is this means different members have already adopted different approaches, and you need to standardise. Ruling that #if is the preferred choice means that code using #ifdef will compile -and run- even when DEBUG_ENABLED is false. And it's much easier to track down and remove debug output that is produced when it shouldn't be than vice-versa.
Oh, and a minor readability point. You should be able to use true/false rather than 0/1 in your #define, and because the value is a single lexical token, it's the one time you don't need parentheses around it.
#define DEBUG_ENABLED true
instead of
#define DEBUG_ENABLED (1)
They're both hideous. Instead, do this:
#ifdef DEBUG
#define D(x) do { x } while(0)
#else
#define D(x) do { } while(0)
#endif
Then whenever you need debug code, put it inside D();. And your program isn't polluted with hideous mazes of #ifdef.
#ifdef just checks if a token is defined, given
#define FOO 0
then
#ifdef FOO // is true
#if FOO // is false, because it evaluates to "#if 0"
We have had this same problem across multiple files and there is always the problem with people forgetting to include a "features flag" file (With a codebase of > 41,000 files it is easy to do).
If you had feature.h:
#ifndef FEATURE_H
#define FEATURE_H
// turn on cool new feature
#define COOL_FEATURE 1
#endif // FEATURE_H
But then You forgot to include the header file in file.cpp:
#if COOL_FEATURE
// definitely awesome stuff here...
#endif
Then you have a problem, the compiler interprets COOL_FEATURE being undefined as a "false" in this case and fails to include the code. Yes gcc does support a flag that causes a error for undefined macros... but most 3rd party code either defines or does not define features so this would not be that portable.
We have adopted a portable way of correcting for this case as well as testing for a feature's state: function macros.
if you changed the above feature.h to:
#ifndef FEATURE_H
#define FEATURE_H
// turn on cool new feature
#define COOL_FEATURE() 1
#endif // FEATURE_H
But then you again forgot to include the header file in file.cpp:
#if COOL_FEATURE()
// definitely awseome stuff here...
#endif
The preprocessor would have errored out because of the use of an undefined function macro.
For the purposes of performing conditional compilation, #if and #ifdef are almost the same, but not quite. If your conditional compilation depends on two symbols then #ifdef will not work as well. For example, suppose you have two conditional compilation symbols, PRO_VERSION and TRIAL_VERSION, you might have something like this:
#if defined(PRO_VERSION) && !defined(TRIAL_VERSION)
...
#else
...
#endif
Using #ifdef the above becomes much more complicated, especially getting the #else part to work.
I work on code that uses conditional compilation extensively and we have a mixture of #if & #ifdef. We tend to use #ifdef/#ifndef for the simple case and #if whenever two or more symbols are being evaluation.
I think it's entirely a question of style. Neither really has an obvious advantage over the other.
Consistency is more important than either particular choice, so I'd recommend that you get together with your team and pick one style, and stick to it.
I myself prefer:
#if defined(DEBUG_ENABLED)
Since it makes it easier to create code that looks for the opposite condition much easier to spot:
#if !defined(DEBUG_ENABLED)
vs.
#ifndef(DEBUG_ENABLED)
It's a matter of style. But I recommend a more concise way of doing this:
#ifdef USE_DEBUG
#define debug_print printf
#else
#define debug_print
#endif
debug_print("i=%d\n", i);
You do this once, then always use debug_print() to either print or do nothing. (Yes, this will compile in both cases.) This way, your code won't be garbled with preprocessor directives.
If you get the warning "expression has no effect" and want to get rid of it, here's an alternative:
void dummy(const char*, ...)
{}
#ifdef USE_DEBUG
#define debug_print printf
#else
#define debug_print dummy
#endif
debug_print("i=%d\n", i);
#if gives you the option of setting it to 0 to turn off the functionality, while still detecting that the switch is there.
Personally I always #define DEBUG 1 so I can catch it with either an #if or #ifdef
#if and #define MY_MACRO (0)
Using #if means that you created a "define" macro, i.e., something that will be searched in the code to be replaced by "(0)". This is the "macro hell" I hate to see in C++, because it pollutes the code with potential code modifications.
For example:
#define MY_MACRO (0)
int doSomething(int p_iValue)
{
return p_iValue + 1 ;
}
int main(int argc, char **argv)
{
int MY_MACRO = 25 ;
doSomething(MY_MACRO) ;
return 0;
}
gives the following error on g++:
main.cpp|408|error: lvalue required as left operand of assignment|
||=== Build finished: 1 errors, 0 warnings ===|
Only one error.
Which means that your macro successfully interacted with your C++ code: The call to the function was successful. In this simple case, it is amusing. But my own experience with macros playing silently with my code is not full of joy and fullfilment, so...
#ifdef and #define MY_MACRO
Using #ifdef means you "define" something. Not that you give it a value. It is still polluting, but at least, it will be "replaced by nothing", and not seen by C++ code as lagitimate code statement. The same code above, with a simple define, it:
#define MY_MACRO
int doSomething(int p_iValue)
{
return p_iValue + 1 ;
}
int main(int argc, char **argv)
{
int MY_MACRO = 25 ;
doSomething(MY_MACRO) ;
return 0;
}
Gives the following warnings:
main.cpp||In function ‘int main(int, char**)’:|
main.cpp|406|error: expected unqualified-id before ‘=’ token|
main.cpp|399|error: too few arguments to function ‘int doSomething(int)’|
main.cpp|407|error: at this point in file|
||=== Build finished: 3 errors, 0 warnings ===|
So...
Conclusion
I'd rather live without macros in my code, but for multiple reasons (defining header guards, or debug macros), I can't.
But at least, I like to make them the least interactive possible with my legitimate C++ code. Which means using #define without value, using #ifdef and #ifndef (or even #if defined as suggested by Jim Buck), and most of all, giving them names so long and so alien no one in his/her right mind will use it "by chance", and that in no way it will affect legitimate C++ code.
Post Scriptum
Now, as I'm re-reading my post, I wonder if I should not try to find some value that won't ever ever be correct C++ to add to my define. Something like
#define MY_MACRO ##################
that could be used with #ifdef and #ifndef, but not let code compile if used inside a function... I tried this successfully on g++, and it gave the error:
main.cpp|410|error: stray ‘#’ in program|
Interesting.
:-)
That is not a matter of style at all. Also the question is unfortunately wrong. You cannot compare these preprocessor directives in the sense of better or safer.
#ifdef macro
means "if macro is defined" or "if macro exists". The value of macro does not matter here. It can be whatever.
#if macro
if always compare to a value. In the above example it is the standard implicit comparison:
#if macro !=0
example for the usage of #if
#if CFLAG_EDITION == 0
return EDITION_FREE;
#elif CFLAG_EDITION == 1
return EDITION_BASIC;
#else
return EDITION_PRO;
#endif
you now can either put the definition of CFLAG_EDITION either in your code
#define CFLAG_EDITION 1
or you can set the macro as compiler flag. Also see here.
The first seems clearer to me. It seems more natural make it a flag as compared to defined/not defined.
Both are exactly equivalent. In idiomatic use, #ifdef is used just to check for definedness (and what I'd use in your example), whereas #if is used in more complex expressions, such as #if defined(A) && !defined(B).
There is a difference in case of different way to specify a conditional define to the driver:
diff <( echo | g++ -DA= -dM -E - ) <( echo | g++ -DA -dM -E - )
output:
344c344
< #define A
---
> #define A 1
This means, that -DA is synonym for -DA=1 and if value is omitted, then it may lead to problems in case of #if A usage.
A little OT, but turning on/off logging with the preprocessor is definitely sub-optimal in C++. There are nice logging tools like Apache's log4cxx which are open-source and don't restrict how you distribute your application. They also allow you to change logging levels without recompilation, have very low overhead if you turn logging off, and give you the chance to turn logging off completely in production.
I used to use #ifdef, but when I switched to Doxygen for documentation, I found that commented-out macros cannot be documented (or, at least, Doxygen produces a warning). This means I cannot document the feature-switch macros that are not currently enabled.
Although it is possible to define the macros only for Doxygen, this means that the macros in the non-active portions of the code will be documented, too. I personally want to show the feature switches and otherwise only document what is currently selected. Furthermore, it makes the code quite messy if there are many macros that have to be defined only when Doxygen processes the file.
Therefore, in this case, it is better to always define the macros and use #if.
I've always used #ifdef and compiler flags to define it...
Alternatively, you can declare a global constant, and use the C++ if, instead of the preprocessor #if. The compiler should optimize the unused branches away for you, and your code will be cleaner.
Here is what C++ Gotchas by Stephen C. Dewhurst says about using #if's.
I like #define DEBUG_ENABLED (0) when you might want multiple levels of debug. For example:
#define DEBUG_RELEASE (0)
#define DEBUG_ERROR (1)
#define DEBUG_WARN (2)
#define DEBUG_MEM (3)
#ifndef DEBUG_LEVEL
#define DEBUG_LEVEL (DEBUG_RELEASE)
#endif
//...
//now not only
#if (DEBUG_LEVEL)
//...
#endif
//but also
#if (DEBUG_LEVEL >= DEBUG_MEM)
LOG("malloc'd %d bytes at %s:%d\n", size, __FILE__, __LINE__);
#endif
Makes it easier to debug memory leaks, without having all those log lines in your way of debugging other things.
Also the #ifndef around the define makes it easier to pick a specific debug level at the commandline:
make -DDEBUG_LEVEL=2
cmake -DDEBUG_LEVEL=2
etc
If not for this, I would give advantage to #ifdef because the compiler/make flag would be overridden by the one in the file. So you don't have to worry about changing back the header before doing the commit.
As with many things, the answer depends. #ifdef is great for things that are guaranteed to be defined or not defined in a particular unit. Include guards for example. If the include file is present at least once, the symbol is guaranteed to be defined, otherwise not.
However, some things don't have that guarantee. Think about the symbol HAS_FEATURE_X. How many states exist?
Undefined
Defined
Defined with a value (say 0 or 1).
So, if you're writing code, especially shared code, where some may #define HAS_FEATURE_X 0 to mean feature X isn't present and others may just not define it, you need to handle all those cases.
#if !defined(HAS_FEATURE_X) || HAS_FEATURE_X == 1
Using just an #ifdef could allow for a subtle error where something is switched in (or out) unexpectedly because someone or some team has a convention of defining unused things to 0. In some ways, I like this #if approach because it means the programmer actively made a decision. Leaving something undefined is passive and from an external point of view, it can sometimes be unclear whether that was intentional or an oversight.