Preprocessor directives

About this Post

You have all used preprocessor directives such as #define or #include. However, the less experienced may not really know the difference between preprocessor directives and the actual program code. This article aims to explain this difference, which preprocessor directives are available, how to use them and what alternatives there are. And perhaps there will be some aspects that will also be of interest to the “older hands”. I will cover the following:

What is the preprocessor, what are preprocessor directives?

The preprocessor is a program or component that is used before the actual compilation or execution of a code. It is often used in programming languages such as C and C++ (including Arduino code) to process source code before compilation. Essentially, the preprocessor performs the following tasks:

  • Replacement of strings by macros.
  • Inclusion of files.
  • Conditional inclusion or exclusion of code.
  • Control of compiler behavior (messages, errors, warnings).

Preprocessor directives are the functions of the preprocessor. They are easy to identify as they begin with the hash character #. In contrast to instructions in the actual program code, they are not terminated with a semicolon, but with the end of the line.

Including files – #include

As you know, you integrate files such as library files via #include. This can be written as #include <Dateiname/Pfad> and #include "Dateiname/Pfad":

  • #include <...>: The compiler searches for the file in the standard include paths. These paths usually lead to folders called “libraries” or “include”.
  • #include "...": The compiler first searches for the file in the current directory. If it does not find it there, it continues the search in the standard include paths.

For example, if you have created a file called config.h for your project, there is a good chance that a file with the same name already exists somewhere. In this case, you could avoid including the wrong file by saving your config.h in the folder of your “.ino” file and including it via #include "..."

Macros – #define

Simple macros without passing variables

To create a macro, use #define. In its simplest form, it looks like this:

#define identifier replacement

e.g.:

#define LED_PIN 10

By simple macros, I mean macros without any further passing of variables. In this case, #define basically does the same as the search and replace function of a word-processing program. The identifier (officially: the macro name) LED_PIN is replaced by 10 everywhere in your program. In the compiled program, it is no longer possible to see whether the 10 or the identifier was used in the source code.

On “spelling”: it is common practice to write macro names in capital letters. Of course, it also works with lower case letters, but you will be doing yourself and others a favor if you stick to such conventions.

In the depths of the libraries belonging to the Arduino package, you will see many macro names that begin and sometimes end with one or two underscores, such as “__AVR_ATmega328P__”, “_VECTORS_SIZE” or “__PACKED_STRUCT”. The underscores provide a certain level of protection – as long as you do not use them yourself.

Pitfalls with simple macros

The “replacement”, i.e. the “macro value”, only gets its data type in the context of the program code. Which data type you mean and which data type the compiler makes of it may be two different things. See the following little sketch:

#define A_DEF 30000
#define B_DEF 30000U
/* alternative 1: */
// static const int A_DEF = 30000;
// static const unsigned int B_DEF = 30000;
/* alternative 2: */
// static constexpr int A_DEF = 30000;
// static constexpr unsigned int B_DEF = 30000;


void setup() {
    Serial.begin(115200);
    // delay(2000); 
     
    Serial.print("A_DEF + 10000: ");
    Serial.println(A_DEF + 10000); 
    Serial.print("B_DEF + 10000: ");
    Serial.println(B_DEF + 10000); 
}

void loop() {}

And here is the output on an ATmega328P-based board:

Preprocessor directives - output out_of_range.ino
Output of out_of_range.ino

A_DEF is obviously interpreted as a signed integer. Since I have chosen a board on which an integer has a size of 2 bytes, there is an overflow and correspondingly a negative result. Although the compiler issues a warning, it is easy to overlook. The numeric literal operator “U” in the definition of the macro B_DEF provides a remedy, as it tells the compiler that the value should be “unsigned”. Alternatively, you can append “L” for long, “UL” for unsigned long, F for float or D for double. Lower case is also possible.  

Another obvious pitfall is the name collision. The compiler warns of a “redefinition” in such a case, but it does not lead to termination. Therefore, it can also easily be overlooked.

Alternatives to #define (const/constexpr, enum)

const and constexpr

The use of constants (const) or constant expressions (constexpr) is preferable to simple #define directives, especially for larger programs. They express much more clearly what the values mean.  

On the difference const vs. constexpr: A constant const may be assigned its value during runtime, with a constant expression constexpr must happen during compilation.

The addition static ensures that a constant is only initialized once, and static can also prevent the constant from being visible in other files. The exact effect of static depends on where exactly the constant has been defined (e.g. inside or outside of functions and classes). However, this goes too far here.

enum

You use an enumeration enum for constants that form a group, such as the primary colors in this example:

enum COLOR {
    RED   = 0xFF000,  // RGB-value
    GREEN = 0x00FF00,
    BLUE  = 0x0000FF
};

void setup() {
  Serial.begin(115200);
  COLOR myFavoriteColor = GREEN;
  if (myFavoriteColor == GREEN){
    Serial.println("My favorite color is green");
  }
}

void loop () {}

Often it is not a matter of assigning specific values to the enum elements, but simply of having an identifier, e.g:

enum DAY_OF_THE_WEEK {
    MON, TUE, WED, THU, FRI, SAT, SUN
};

Macros with passing variables

The #define macros can also take variables. This still makes them a search and replace tool, but a more complex version. Here are some examples that also show a few pitfalls:

#define A_PLUS_B(a,b) a+b
#define A_PLUS_B_BETTER(a,b) (a+b)
#define SQUARE(a) a*a
#define SQUARE_BETTER(a) (a)*(a)
#define MAP_RANGE(a) map(a, 0, 100, 0, -500)
#define MAKE_STRING(a) #a
#define MERGE(a,b) a##b

void setup() {
    Serial.begin(115200);
    // delay(2000); 
     
    Serial.print("3 * A_PLUS_B(1,2):        ");
    Serial.println(3 * A_PLUS_B(1,2));
    
    Serial.print("3 * A_PLUS_B_BETTER(1,2): ");
    Serial.println(3 * A_PLUS_B_BETTER(1,2));

    Serial.print("SQUARE(1+2):              ");
    Serial.println(SQUARE(1+2));

    Serial.print("SQUARE_BETTER(1+2):       ");
    Serial.println(SQUARE_BETTER(1+2));

    Serial.print("MAP_RANGE(50):            ");
    Serial.println(MAP_RANGE(50));

    Serial.print("MAKE_STRING(ABC123):      ");
    Serial.println(MAKE_STRING(ABC123));
    
    String mergedString(MERGE(1,2)); // = String mergedString(12);
    Serial.print("MERGE(1,2) as String:     ");
    Serial.println(mergedString);
    
    int mergedInt = MERGE(1,2); // = int mergedInt = 12;
    Serial.print("MERGE(1,2) as Integer:    ");
    Serial.println(mergedInt);  
}

void loop() {}
 

Here is the output:

Preprocessor directives - output of macros_with_variables.ino
Output of macros_with_variables.ino

And here are a few explanations:

  • A_PLUS_B(1,2) is replaced by 1+2.
    • Pitfall: 3*1+2 is 5!
  • A_PLUS_B_BETTER(1,2) is replaced by (1+2).
    • So: 3*(1+2) = 9.
  • SQUARE(1+2) is replaced by 1+2*1+2 → pitfall!
  • SQUARE_BETTER(1+2) is replaced by (1+2)*(1+2) and returns the expected result.
  • MAP_RANGE(50) is replaced by map(50, 0, 100, 0, -500);.
  • MAKE_STRING(ABC123) is replaced by the character string ABC123
    • The hash character # preceeding the “a” is an operator that instructs the preprocessor to replace “a” with a character string.
  • MERGE(1,2) is replaced by 12.
    • The operator ## concatenates a and b.
    • The datatype of “12” is determined by the context (here: integer or string)

macros vs. functions

In the last example, we passed integer values to the macro A_PLUS_B as parameters. The cool thing is that the macro also works with float variables or strings. And the whole thing in a single-line definition! To achieve the same with a function would be a bit more complex. You can either solve this by overloading:

void setup() {
    Serial.begin(115200);

    int aInt = 1;
    int bInt = 2; 
    int cInt = aPlusB(aInt, bInt);
    Serial.print("aPlusB(aInt,bInt) = ");
    Serial.println(cInt);

    String aStr = "1";
    String bStr = "2";
    String cStr = aPlusB(aStr, bStr); 
    Serial.print("aPlusB(aStr,bStr) = ");
    Serial.println(cStr);    
}
void loop() {}

int aPlusB(int a, int b) {
    return a+b;
}

String aPlusB(String a, String b) {
    return a+b;
}

… or you apply templates:

void setup() {
    Serial.begin(115200);

    int aInt = 1;
    int bInt = 2; 
    int cInt = aPlusB<int>(aInt, bInt);
    Serial.print("aPlusB(aInt,bInt) = ");
    Serial.println(cInt);

    String aStr = "1";
    String bStr = "2";
    String cStr = aPlusB<String>(aStr, bStr); 
    Serial.print("aPlusB(aStr, bStr) = ");
    Serial.println(cStr);    
}
void loop() {}

template <typename T>
T aPlusB(T a, T b) {    
    return a+b;
}

Both variants provide the following output:

Output on overload.ino and template.ino
Output of overload.ino and template.ino

Overloading and templates require more effort, but these variants are simply safer. The following, for example, is accepted by the macro A_PLUS_B (or the compiler) without complaint:

String a = "1";
int b = 2;
Serial.println(A_PLUS_B(a, b));

Maybe that’s what you wanted – or maybe it was an oversight.

“Arduino macros”

The Arduino program package makes extensive use of macros. Some of them just shall make the program code easier to understand. For example, LOW and INPUT simply mean 0, OUTPUT and HIGH are 1.

Then there is a series of “function-macros”. A simple example is the “min” macro, which returns the smaller of two values you passed to it:

#define min(a,b) ((a)<(b)?(a):(b))

However, some of the macros are also quite complex and make use of other macros. One example of this is the “F” macro that you know from Serial.print(F("......"));:

#define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))

It uses the PSTR macro:

#define PSTR(s) ((const PROGMEM char *)(s))

Predefined macros

Predefined macros are part of the preprocessor and are not defined via #define. The most common predefined macros can be found in the following sketch:

void setup() {
    Serial.begin(115200);
    Serial.print("Date: ");
    Serial.println(__DATE__); // Compiling date
    Serial.print("Time: ");
    Serial.println(__TIME__); // Compiling time
    Serial.print("File: ");
    Serial.println(__FILE__); // Compiled file
    Serial.print("Line: ");
    Serial.println(__LINE__); // Current line
    Serial.print("C++ : ");
    Serial.println(__cplusplus); // C++ Version (201103 = ISO C++ 2011)
}

void loop() {}

Here is the output:

Predefined preprocessor directives - output predef_macros.ino
Output predef_macros.ino

Conditional inclusion of code – #ifdef, #ifndef & Co

You can use #define to include parts of the source code or have them ignored by the compiler.

#ifdef MACRO_NAME checks whether MACRO_NAME has been defined or not. If MACRO_NAME has been defined, the subsequent lines of code are included. With #ifndef it is exactly the opposite.  

In the simplest case, there is a #endif in the rest of the source code, which ends the conditional inclusion. However, branching is also possible with #else or #elif. In principle, it is exactly as you know it from if, else and else if. Nesting is also possible. However, this can quickly become confusing.

Equivalent to #ifdef MACRO_NAME is #if defined(MACRO_NAME). This notation is used for logical operations, such as #if defined(NAME_1) && defined(NAME_2). You can also use #if to check other logical conditions, such as #if A>B or #if !(A==B).

At the end, each #if, #ifdef or #ifndef must be completed by #endif

A #undef MACRO_NAME does what you would expect: it terminates the definition of MACRO_NAME.

Here is an example sketch:

#define YIN
#define YANG

void setup() {
    Serial.begin(115200);
    // delay(2000); 

#ifdef YIN  // or: #if defined(YIN)
    Serial.println("YIN is defined");
#elif defined(YANG)
    Serial.println("YIN is not defined, but YANG");
#endif

#ifdef YANG
    Serial.println("YANG is defined");
#else 
    Serial.println("YANG is not defined, YIN might be defined");
#endif

#ifndef YANG
    Serial.println("YANG is not defined");
#endif

#if defined(YIN) || defined(YANG)
    Serial.println("YIN and / or YANG are defined");
#endif

#if defined(YIN) && defined(YANG)
    Serial.println("YIN and YANG are defined");
#elif defined(YIN) 
    Serial.println("Only YIN is defined");
#endif

#undef YANG
#ifndef YANG
    Serial.println("YANG is not defined (anymore?)");
#endif

}

void loop() {}

 

The sketch provides the following output:

Preprocessor directives - output conditional_inclusion.ino
Output of conditional_inclusion.ino

If you like, you can now comment out #define YIN and / or #define YANG and see how the output changes.

Application example 1: Avoiding double reading of files

As a concrete (arbitrary) example, let’s take a look at the SPI library file SPI.h of the Arduino Renesas package (you can find it here). Many libraries of SPI-based components include SPI.h automatically. If you use several components, SPI.h could be read several times. This is prevented by enclosing the entire code in SPI.h with the following construction of directives:

#ifndef _SPI_H_INCLUDED
#define _SPI_H_INCLUDED
...... Code ........
#endif

_SPI_H_INCLUDED can only be defined once, and therefore the code can only be read once.

Application example 2: Debugging

In the development phase of a project, it can be useful to output intermediate values, sensor settings, status data or similar on the serial monitor. On the other hand, Serial.print() instructions in particular are both slow and memory-hungry. In such cases, it is advisable to “switch on” or “switch off” this code using conditional inclusion.   There are many libraries that offer such a debug option. 

However, there is another pitfall here. Take a look at the following simple example. The main sketch uses the function sumUp(), which is defined in sum.h and sum.cpp. sumUp() simply adds three numbers. If DEBUG is defined, the intermediate steps should be output, otherwise only the final result.  

#define DEBUG
#include "sum.h"
#define SUMMAND_1 17
#define SUMMAND_2 4
#define SUMMAND_3 42

void setup() {
    Serial.begin(115200);
    int value = sumUp(SUMMAND_1, SUMMAND_2, SUMMAND_3); 
    Serial.print("Sum: ");
    Serial.println(value);
}

void loop() {}
// #define DEBUG
// #include "sum_config.h"
#include "Arduino.h"
#ifndef _SUM_UP
#define _SUM_UP
int sumUp(int, int, int);
#endif
#include "sum.h"
int sumUp(int s1, int s2, int s3){
    int result = 0;
    result += s1;
#ifdef DEBUG
    Serial.print("Interim sum 1: ");
    Serial.println(result);
#endif
    result += s2;
#ifdef DEBUG
    Serial.print("Interim sum 2: ");
    Serial.println(result);
#endif
    result += s3;
    return result;
}
#ifndef _SUM_CONF
#define _SUM_CONF
// uncomment the following line to activate DEBUG
// #define DEBUG
#endif

If you run the sketch as it is, you may expect the detailed output (bottom left), as DEBUG is defined in the main sketch. However, you will get the output on the right.

Output of sum_main.ino with and without DEBUG
Output of sum_main.ino with and without DEBUG

The problem is that the preprocessor does not adhere to the sequence, but executes the #include directives first. You can solve the problem by uncommenting #define DEBUG in sumUp.h. To make it easier for the user and to minimize the risk of accidentally commenting out something incorrect, configuration files are often used in such cases. Therefore, alternatively uncomment #include "sum_config.h" in sum.h and #define DEBUG in sum_config.h.

Application example 3: MCU-specific code

The Arduino ecosystem tries to make code as universal as possible, i.e. independent of the board or microcontroller (MCU) used. Nevertheless, there are things that do not work (equally) everywhere, such as SoftwareSerial. As a concrete example, let’s take a look at an example sketch of the DFRobotDFPlayerMini library(click here for the full sketch).

#if (defined(ARDUINO_AVR_UNO) || defined(ESP8266))   // Using a soft serial port
#include <SoftwareSerial.h>
SoftwareSerial softSerial(/*rx =*/4, /*tx =*/5);
#define FPSerial softSerial
#else
#define FPSerial Serial1
#endif

Only if the board is an AVR-based Arduino UNO (e.g. R3) or an ESP8266-based board, SoftwareSerial is included and the object softSerial is created, otherwise Serial1 (HardwareSerial) is used. softSerial or Serial1 are renamed to FPSerial so that no distinction needs to be made between HardwareSerial and SoftwareSerial in the further course.

However, the example also shows that there are pitfalls. For example, the sketch does not work with an Arduino Nano (Classic) or an Arduino Pro Mini. They are not an Arduino UNO, they are not ESP8266-based, they do not have Serial1 and therefore fall through the cracks. I would rather have chosen __AVR_ATmega328P__ or ARDUINO_ARCH_AVR as ARDUINO_AVR_UNO.

Where can I find the board or MCU #defines?

Unfortunately, there is not the one big table where you can find all the definitions for the different architectures, boards and microcontrollers (at least I haven’t found one). But you can help yourself in other ways:

Set up the board of your choice in the Arduino IDE and compile any sketch. Then click on the window with the compiler messages and click CTRL/f. Enter “-D” in the search window. This marks the places where definitions are located (their origin is: board.txt and platform.txt). You might have to scroll to the right to find the marked locations.

For a WEMOS D1 Mini Board I found the definitions in the following line (shortened output):

C:\Users\Ewald\AppData\Local\Arduino15\packages\esp8266\……..-DESP8266…..-DARDUINO_ESP8266_WEMOS_D1MINI -DARDUINO_ARCH_ESP8266…..

This is what it looked like on the screen:

Excerpt of compiler messages for WEMOS D1 mini board

What you will not find in this way, for example, are the definitions for the various AVR microcontrollers. The io.h file from the compiler files can help here. Where exactly it is located depends on your installation. Look where your “packages” are located. For me, the path looks like this:

C:\Users\Ewald\AppData\Local\Arduino15\packages\arduino\tools\avr-gcc\7.3.0-atmel3.6.1-arduino5\avr\include\avr\io.h

Here is a compilation of definitions for various boards:

Preprocessor directives: #defines for different boards
Examples of board and microcontroller definitions

Warnings and errors – #warning, #error

With the preprocessor directive #warning – surprise! – you issue a warning. You will normally attach a condition to this. The warning has no further consequences for the compilation process.

The preprocessor directive #error does the same as #warning, but leads to a compiler abort. Here is a simple example:

#define ARRAY_SIZE 10
#define NUMBER_OF_ARRAYS 42
#if ARRAY_SIZE * NUMBER_OF_ARRAYS > 400
#warning You might run out of memory!
// #error You will run out of memory!
#endif

void setup() {}

void loop() {}

These are the resulting outputs:

Example output Preprocessor directive #warning
#warning output
Example output Preprocessor directive #error
#error output

Manipulating the line counter and file name – #line

The preprocessor “knows” which file and which program line it is currently processing. You can change both with #line line_number "file_name". However, I don’t know why one should do this (except to annoy others!).

However, here is an example:

void setup() {
//#line 33 "blabla.h"    
    int i = 42!;
}

void loop() {}

This is the normal output:

“Normal” error message

And here is the manipulated version:

Error message, manipulated with #line

The multifunctional tool – #pragma

The #pragma directives are versatile, but not standardized. This means that every compiler implementation can in principle define its own #pragma directives. On the one hand, this means that I cannot provide a complete overview, and on the other hand, it means that I cannot guarantee that all the directives presented here are compatible with every compiler. However, it should work with the compilers of the Arduino IDE for the common boards (e.g. AVR, Renesas, ESP32, ESP8266, ARM).  

#pragma once

A simple #pragma once at the beginning of a file ensures that it is only read in once. This is much simpler than the conventional method of enclosing the entire file content with a #ifndef#defendif construction.

#pragma poison

You can use #pragma poison to mark identifiers, functions or character strings as “poison”. The compiler aborts when it encounters these. Here is an example:

#pragma GCC poison blablabla

void setup() {
    Serial.begin(115200);
    String blablabla = "Nice to see you";
}

void loop() {}
#pragma preprocessor directives - output pragma_poison.ino
Output of pragma_poison.ino

This also works with begin or “Nice to see you”, but not simply with Nice.

#pragma message / warning / error

The directive #pragma message "xxxxxxx" outputs the message “xxxxxxx” during compilation. The compilation process is not aborted.  It is the same with #pragma GCC warning. #pragma GCC error, on the other hand, outputs an error and the compiler aborts.

void setup() { 
    Serial.begin(115200); 
    String blabla = "The weather is nice";
    Serial.println(blabla); 
#pragma message "This is blabla"
    Serial.println(blabla);
#pragma GCC warning "Again, this was blabla"
    Serial.println(blabla);
// #pragma GCC error "Refusing to compile this blabla any longer!"
}

void loop(){}

The output is:

#pragma preprocessor directives - Output pragma_msg_warn_err.ino
Output 1 of pragma_msg_warn_error.ino

If you now uncomment line 9, you will only get the error message:

#pragma preprocessor directives - Issue 2 of pragma_msg_warn_err.ino
Output 2 of pragma_msg_warn_error.ino

#pragma diagnostic

Suppressing warnings

The #pragma GCC diagnostic directive is used to control warnings and error messages. The various options are selected using additional parameters. #pragma GCC diagnostic ignored <warning type> suppresses the warning of the type “warning type”.

As an example, I return to the overflow problem that occurred with the following code (for boards with an integer size of 2 bytes):

//#pragma GCC diagnostic ignored "-Woverflow"
#define A 30000
#define B 10000
void setup() { 
    Serial.begin(115200); 
    Serial.println(A + B);
}

void loop(){}

You can suppress the following warning by uncommenting the first line of the sketch:

Output pragma_dignostic.example
Output of pragma_diagnostic.ino

Suppressing warnings in a defined area / making errors from warnings

With #pragma GCC diagnostic error <warning-type> you can turn a compiler warning into a compiler error, i.e. the compiler aborts its work.

Let’s take a look at this by the example of unused variables. You will receive a warning for these if you have selected either “More” or “All” under “Compiler warnings” in the Arduino IDE settings. With #pragma GCC diagnostic error "-Wunused-variable" we make an error out of it in the next sketch. As we saw before, we can suppress the message again with #pragma GCC diagnostic ignore "Wunused-variable".

However, we can limit the suppression of the message to a selected area. This is done using #pragma GCC diagnostic push and #pragma GCC diagnostic pop. With push the compiler remembers the current warning setting, with pop it restores this state. It may sound complicated, but it is simple:

void setup() { 
    Serial.begin(115200);
#pragma GCC diagnostic error "-Wunused-variable" // makes an error out of warning
    int usedVar = 42;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable" 
    int unusedVar = 43;  // error is inored here
#pragma GCC diagnostic pop
    int anotherUnusedVar = 44;  // error is not ignored here
    Serial.println(usedVar);
}

void loop(){}

Here is the output:

#pragma preprocessor directives - Output pragma_diag_push_pop.ino
Output of pragma_diag_push_pop.ino

If you comment lines 5 and 8, you will not get an error message. If you comment out lines 5, 6 and 8, you will get two error messages.

#pragma pack()

The following sketch defines a structure exampleStruct, which consists of two uint8_t and two uint32_t elements. The structure should therefore use 10 bytes of memory. The sketch outputs the actual memory used.

//#pragma pack(1)
struct exampleStruct{
    uint32_t    intVal_1;    //   4 Byte
    uint8_t     byteVal_1;   // + 1 Byte
    uint32_t    intVal_2;    // + 4 Byte
    uint8_t     byteVal_2;   // + 1 Byte = 10 Byte (in theory!)
};

void setup() { 
    Serial.begin(115200);
    Serial.print("Size of exampleStruct: ");
    Serial.println(sizeof(exampleStruct));
}

void loop(){}

If you run the sketch on an AVR-based board such as the Arduino UNO R3, you will get the expected 10 bytes. If you now switch to an ESP32 board or an Arduino UNO R4 (Minima or WIFI), for example, the output may surprise you:

#pragma preprocessor directives - Output pragma_pack.ino
Output pragma_pack.ino on an ESP32

What happens here is called padding. The larger 32-bit microcontrollers work faster if the elements of the structure are packed into four-byte blocks in such a way that, if possible, no element is spread over two or more blocks. The blocks are boxes with four compartments, so to speak, with one byte fitting into each compartment. The element intVal_1 fills the first box. A new box is used for byteVal_1. The three free compartments are not sufficient for intVal_2, which is why a new box is used. byteVal_2 needs another new box, the same applies to intVal_2 → makes four boxes = 16 compartments = 16 bytes.

If you arrange the elements differently, you can save space, e.g: intVal_1byteVal_1byteVal_2intVal_2. Then byteVal_2 still fits into the second box, and you only use three boxes in total = 12 bytes.

You can use #pragma pack(x) to specify the number of compartments x per box. Here x must be 2n, i.e. x = 0, 2, 4, etc. With #pragma pack(1) you achieve the maximum packing density. Uncomment line 1 in the example sketch above to limit the size of exampleStruct to 10 bytes. If you select #pragma pack(2), the result is 12 bytes. The default setting therefore corresponds to #pragma pack(4).

2 thoughts on “Preprocessor directives

  1. Although I’ve been using the “C” preprocessor for decades, I still learned a lot from this excellent article.

    I greatly appreciate the excellent work you do.

Leave a Reply

Your email address will not be published. Required fields are marked *