Are there any traps with using compiler-conditions #if defined()?

Today I had a code that sometimes is used with an I2C-LC-Display

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,20,4);  // adjust to the right I2C of your device
  lcd.init();                      // initialize the lcd
  lcd.backlight();                 //Backlight ON if under program control


and

and some other times used with a non-I2C-LC-Display

#include <LiquidCrystal.h>
//LiquidCrystal lcd (10, 9, 8, 7, 6, 5);

  lcd.begin(20, 4);

I have seen that there are compiler-conditions example

#if defined(ESP8266)
  /* ESP8266 Dependencies */
  #include <ESP8266WiFi.h>
  #include <ESPAsyncTCP.h>
  #include <ESPAsyncWebServer.h>
#elif defined(ESP32)
  /* ESP32 Dependencies */
  #include <WiFi.h>
  #include <AsyncTCP.h>
  #include <ESPAsyncWebServer.h>
#endif

I understand how this works but I have not yet used it myself.
Before using it myself I want to ask if there are any traps I could fall in
by using compiler-conditions

I guess with this stuff you could write code that is more complicated to analyse than brainf**k because it adds a new level of complexity which lines have been compiled and which not??????

So as long as I use it with some simple symbols

#define mySymbol

#if defined(mySymbol)

void compileThis() {
...
}
#endif

#elif defined(myOtherSymbol)
void useThat() {
...
}
#endif

Does work as expected or are there any traps?

best regards Stefan

It's horribly common in code. It DOES obscure the functionality and can interfere with formatting.

One trap is WRT #if defined(x) vs #if x
I tend to prefer the latter so that #define x 0 works to explicitly disable an option. You can always add:

#if !(defined(x))
# define x 0
#endif

Somewhere.

Also not that #define is local to a single file (unless that file is also #include'd in other files.)

Here is one trap I learned from experience. If you misspell the symbol then it will never be defined.

not sure what you mean by "trap". of course ifdefs or conditional code this can be misused or used improperly.

when properly used, it allows code to work on different hardware, both processors or peripherals, which is much better that having entirely separate programs.

when used properly, it should have a minimal affect. for example, some processor/HW dependent code should be isolated to as few functions as possible and code within those functions made processor/HW dependent.

the #error directive can also be used in an #else case if none of the previous conditions apply. For example, what should happen if you code is compiled for Arduino. #error will generate an error as if it were a compiler error, preventing a success compile that may actually not work.

Yes. (of course) This created a good idea:

As I'm already using this function

void PrintFileNameDateTime() {
  Serial.println("Code running comes from file ");
  Serial.println(__FILE__);
  Serial.print("  compiled ");
  Serial.print(__DATE__);
  Serial.print(" ");
  Serial.println(__TIME__);  
}

that makes the compiled code tells me the source-code-version the compiling was done with

Adding something like this

#if defined(mySymbol)
Serial.println( F("mySymbol is defined") );

tells what conditions / symbols are there
And the missing of this serial output would "tell" it might be mis-spelled
best regards Stefan

if there's an ifdef for a symbol that is not defined , a #else and #error will generate an error during compilation. you won't need to check at run-time

Good to know. Nevertheless I think I will it add it for informational purposes.

best regards Stefan

from work we learned to have a way to identify any version of code the left our lab. we just used date codes (e.g. 210609e). i would find i'm not using the code i just tried compiling.

when testers found problems, we would first ask what version and we often found out they weren't using the correct version.

so when you consider identifying your code, you might consider adding a version as well. you may find it saving you hours of frustration

the PrintFileNameDateTime shall do that. Though the disadvantage is that the source-code-file-timestamp could be modified through simply saving it again.

Setting a firmware-version-code before delivering or publishing is a very common thing.
And after a lot of testing it is worth the effort to manually change the firmware-number.
But I don't do that for each minor or temporarly changes. That's why I use the filename and file-timestamp
best regards Stefan

Typically you would use symbols already defined by the compiler or pass them as arguments during compilation.

Otherwise the order the files are processed can be relevant.

Edit:

As an example in the link below you can find a list of built in macro's for the avr-gcc compiler.

https://gcc.gnu.org/onlinedocs/gcc/AVR-Options.html

For instance:

__AVR_Device__

    Setting -mmcu=device defines this built-in macro which reflects the device’s name. For example, -mmcu=atmega8 defines the built-in macro __AVR_ATmega8__, -mmcu=attiny261a defines __AVR_ATtiny261A__, etc.

    The built-in macros’ names follow the scheme __AVR_Device__ where Device is the device name as from the AVR user manual. The difference between Device in the built-in macro and device in -mmcu=device is that the latter is always lowercase.

    If device is not a device but only a core architecture like ‘avr51’, this macro is not defined.

So to compile something only on an ATtiny85 you would do the following

#if defined(__AVR_attiny85__)
// some stuff
#endif

I dunno. I put #if defined(DEBUG) with a #define DEBUG 1 at the top of an individual file, all the time.

a list of built in macro's for the avr-gcc compiler.

That's some of them. You can see them all (for any particular compile line) by using a magic incantation:

 avr-gcc -dM -E -mmcu=atmega328p - </dev/null

(include any other compiler options that you might be using in the command line, if you want...)
(modify slightly for non-unix-like OSes.)

there are #ifdef, #ifndef and #undef directives

symbols my be intentionally undefined