#define type limits

Ive nociced that code often includes the #define statement to set values that are constant in a sketch. I think that it is useful because they are easy to find.

I have 2 questions about this,

  1. Can they be any value? Ie, no type is set as with a variable, can the defined value effectiveley be a float.

  2. Is there really any difference between a #define statement and a const variable?

#defines are simple text replacements. Whatever is in the define is just replaced where it is used. So it can be a float, string, String, any data type but its type is not relevant. If you define as float it will be converted to text.

The value in the define has no type (byte, int, long). The const value retains its type. So it will occupy that types room in memory. A byte const, 2 bytes, an int, 4 bytes, ......

I prefer to not use #define most of the time and never for constant values.

By using a const variable of a known type, you assure yourself and the compiler that you know what type it should be, instead of allowing the compiler to do as it wishes, which can result in code malfunction when you treat the value inappropriately/carelessly. The examples of this occur daily on this forum, so just stay tuned.
That's a start.

ES.31: Don’t use macros for constants or “functions”

Reason

Macros are a major source of bugs. Macros don’t obey the usual scope and type rules. Macros don’t obey the usual rules for argument passing. Macros ensure that the human reader sees something different from what the compiler sees. Macros complicate tool building.

In the define itself it indeed has no type as it’s just a directive for the preprocessor and as explained already every occurrence of the keyword will be replaced textually by the text that was provided.

At that point the C++ rules apply and a type (when appropriate) will be inferred by the compiler. For example if you have #define value 42, wherever this 42 gets injected it will be treated as an int as this is the default type for integral literals (if it fits).

That’s where the proverbial sh*t can hit the fan because of type constrained operations. On a UNO for example an int is on 16 bits and the max value is 32767. If you do

#define oneSecond 1000 // ms
#define oneMinute (60*oneSecond)

The maths in oneMinute will be conducted as 16bits value and 60000 does not fit and you’ll rollover and get a wrong value.

Contrast that with typed constants If you do

constexpr unsigned long oneSecond = 1000; // ms
constexpr unsigned long oneMinute = (60*oneSecond);

Then the compiler knows oneSecond is of type unsigned long and the multiplication is conducted as unsigned long and thus oneMinute has the right value.

(You could use suffix like U UL ULL…) to provide more info on the literal’s type but that does not make the use of #define better. You should just keep that for conditional compile options.)

1 Like

@J-M-L , thank you for clearing up my misunderstanding. I see that i have some studying to do.

Thanks all, that has made it a lot clearer. Although, it does raise the question of what they should really be used for.

The link from PieterP is useful.

It appears that simplistically, a #define is a compile time "find and replace" which can be generally omitted in lieu of more precise methods.

as I said - conditional compilation is one of the use

for example with the ESPAsyncWebServer library, you need to include a different WiFi library depending if you are running on an ESP32 or an ESP8266

The IDE has an hidden #define for ESP32 or for ESP8266 at compile time that you can catch in the file when it's compiled. So you would use

#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

AsyncWebServer server(80);

and your program would be ready to be compiled on both architectures.

another common example is for debug. I use

#define DEBUG 1    // SET TO 0 TO REMOVE TRACES

#if DEBUG
#define D_SerialBegin(...) Serial.begin(__VA_ARGS__);
#define D_print(...)       Serial.print(__VA_ARGS__)
#define D_write(...)       Serial.write(__VA_ARGS__)
#define D_println(...)     Serial.println(__VA_ARGS__)
#else
#define D_SerialBegin(...)
#define D_print(...)
#define D_write(...)
#define D_println(...)
#endif

in your code you use for example D_println(F("this is a debug trace")); and if you defined DEBUG to be 1 then the preprocessor will replace that with Serial.println(F("this is a debug trace")); and you'll see the trace in the serial monitor.
But if you defined DEBUG to be 0, then D_println is defined to be empty and the whole line D_println(F("this is a debug trace")); just goes away when you compile.

âžś that's an easy way to remove debug code from your project once everything is working (really 0 memory will be used)

1 Like

You've mentioned the debug method on a reply to an earlier problem that I had. I use that a lot now and is very useful.

Am i correct in saying that when using #if and #else, only 1 will be compiled, unlike the standard if and else?

1 Like

@iangill
yes, it's called "conditional compilation" for that reason.

Take a browse through many of the .h and .hpp files in the libraries you've loaded, you'll see a plethora of examples of use of the #if, #else, and other valid constructs and how they're used. It's quite illustrative, and you'll find more uses as you note other keywords.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.