Preprocessor Macros V.S. Inline Functions

General introduction and comparison between macors and inline functions.

Macros

Macros rely on textual substitution. The preprocessor macros are just substitution patterns in code before the compilation, so there is no type-checking at that time. However, the compiler still does type checking after macros had been expanded.

That is, the text defined in macros will be substituted by preprocessor. For example,

#include <stdio.h>
#define SQUARE(x) x*x

int main() {
  int s = SQUARE(5); // This will be converted to: int s = 5*5;
  printf("%d\n", s);
  return 0;
}

By #define SQUARE(x) x*x, the pattern of SQUARE(x) in the code will be replaced by x*x. Therefore, the int s = SQUARE(5); will be converted to int s = 5*5.

int main() {
  printf("%d\n", SQUARE(3+2));
  printf("%lf\n", (double)1/SQUARE(5));
  return 0;
}

The result for #define SQUARE(x) x*x is:

11
1.000000

obviously, #define SQUARE(x) x*x will cause error when we apply SQUARE(3+2) and 1/SQUARE(5). We will get 3+2*3+2(11) and 1/5*5(1.00) rather than 25 and 0.04.

Try #define SQUARE(x) (x)*(x):

25
1.000000

The (double)1/SQUARE(5) will be converted to (double)1/(5)*(5)(1), that is still wrong.

The answer is #define SQUARE(x) ((x)*(x)):

25
0.040000

It seems that we solve the problem. However, it does NOT. The result of

int a = 5;
printf("%d\n", SQUARE(a++)); // expected output is 25
printf("%d\n", a);  // expected output is 6

is

30
7

The substitution of SQUARE(a++) is ((a++)*(a++)). So, they will be5*6, and then a = 7 after this statement. The first a = a + 1 run immediately after 5*(...). In the same manner, the second a = a + 1 run immediately after 5*6. It's execution order may be similar to

a = 5;
int result;
result = a;
a = a + 1;
result = result * a;
a = a + 1;
return result;

The following is one solution for our problem using GCC.

#define SQUARE(x) ({    \
  typeof (x) _x = (x);  \
  _x * _x;              \
})

The typeof is a non-standard GNU extension to declare a variable having the same type as another. Thus, typeof(x) _x = (x); will be turned into int _x = (x++);. See more typeof here and discussion about #define Square(x) here.

When to use macros

There are common cases:

  1. To alleviate the function-call overhead at the potential cost of larger code size.
  2. Define a general function beyond types, such as:
    #define SUM(array, size) ({         \
     typeof(*array) total = 0;       \
     int i = 0;                      \
     for (i = 0 ; i < size ; i++) {  \
       total += array[i];            \
     }                               \
     total;                          \
    })
    

Side effect

If the macros doesn't be defined well, then the behavior would be out of your expectation without any compiler warnings or errors.

#include <stdio.h>
#define MAX(a,b) ((a < b) ? b : a)

int main() {
  int a = 5, b = 10;
  printf("%d\n", MAX(a++, b++)); // expected 10, but output will be 11
  return 0;
}

Safer macros

Please read safer macros; It shows some example to check type in macros.

Inline functions

Inline functions are actual functions whose copy of the function body are injected directly into each place the function is called. The insertion (called inline expansion or inlining) occurs only if the compiler's cost/benefit analysis show it to be profitable. Same as the macros, inline expansion eliminate the overhead associated with function calls.

Inline functions are parsed by the compiler, whereas macros are expanded by the preprocessor.

See more detail here.

When should we use inline functions instead of macros

The preprocessor macros are just substitution patterns in code before the compilation, so there is no type-checking at that time. We might unconsciously use the wrong type for the macro without any debugging hints from the compiler.

Considering the following code:

#include <stdio.h>
#include <stdbool.h>  // for type bool

#define TURN_UP(audio)  ++audio.vol

struct Power {
  bool ac;  // true: Alternating current (AC), false: direct current(DC)
  int vol;  // Voltage
  int amp;  // Ampere
  int freq; // Frequency for AC. This must be zero for DC.
};

struct Audio {
  int freq; // Frequency
  int vol;  // Volume
  int dur;  // Duration
};

int main() {
  // C99 style for initializing struct data
  struct Power u = { .ac = 0, .vol = 5, .amp = 1, .freq = 0 }; // USB
  printf("Power: %s %dV %dA %dHz\n", (u.ac) ? "AC" : "DC", u.vol, u.amp, u.freq);

  struct Audio a = { .freq = 440, .vol = 10, .dur = 5 }; // Note for A4
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  // Turn up the volume of the sound
  TURN_UP(u); // Oops! Typo! It should be TURN_UP(a);
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  return 0;
}

If we unconsciously use TURN_UP(u) instead of TURN_UP(a), then GCC won't give us hints to make it right.

Thus, we expect:

Power: DC 5V 1A 0Hz
Sound: 10DB 440Hz for 5 Seconds
Sound: 11DB 440Hz for 5 Seconds

but we will get:

Power: DC 5V 1A 0Hz
Sound: 10DB 440Hz for 5 Seconds
Sound: 10DB 440Hz for 5 Seconds

This unexpected result will be a serious problem when our code base has hundreds of thousands line. We will have no idea what's going wrong without any clue. It's better to have hints to find out the cause.

#include <stdio.h>
#include <stdbool.h>  // for type bool

struct Power {
  bool ac;  // true: Alternating current (AC), false: direct current(DC)
  int vol;  // Voltage
  int amp;  // Ampere
  int freq; // Frequency for AC. This must be zero for DC.
};

struct Audio {
  int freq; // Frequency
  int vol;  // Volume
  int dur;  // Duration
};

void turn_up(struct Audio*) __attribute__((always_inline));

void inline turn_up(struct Audio* a) {
  ++a->vol;
}

int main() {
  // C99 style for initializing struct data
  struct Power u = { .ac = 0, .vol = 5, .amp = 1, .freq = 0 }; // USB
  printf("Power: %s %dV %dA %dHz\n", (u.ac) ? "AC" : "DC", u.vol, u.amp, u.freq);

  struct Audio a = { .freq = 440, .vol = 10, .dur = 5 }; // Note for A4
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  // Turn up the volume of the sound
  turn_up(&u); // Oops! Typo! It should be turn_up(&a);
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  return 0;
}

On the contrary, if we use inline functions to do it, gcc will give a hint for this problem.

$ gcc test.c
test.c:34:11: warning: incompatible pointer types passing 'struct Power *' to parameter of type 'struct Audio *' [-Wincompatible-pointer-types]
  turn_up(&u); // Oops! Typo! It should be turn_up(&a);
          ^~
test.c:19:35: note: passing argument to parameter 'a' here
void inline turn_up(struct Audio* a) {
                                  ^
1 warning generated.

However, it may not be helpful enough. When we're developing a large software, we will usually have a very long compiler logs. It's very hard to trace.

In this case, giving error is better than warning. Luckily, we can define some warning as error by ourselves. It's a good options for debugging. We can use -Werror flags to make all warnings into errors, or use -Werror= to make the specified warning into an error.

In our case, we can define incompatible-pointer-types as an error. That is,

$ gcc -Werror=incompatible-pointer-types [FILENAME]

The result will be:

test.c:34:11: error: incompatible pointer types passing 'struct Power *' to parameter of type 'struct Audio *'
      [-Werror,-Wincompatible-pointer-types]
  turn_up(&u); // Oops! Typo! It should be turn_up(&a);
          ^~
test.c:19:35: note: passing argument to parameter 'a' here
void inline turn_up(struct Audio* a) {
                                  ^
1 error generated.

Therefore, we can avoid this problem upon finishing compilation. See more detail here

Another case to replace macro by inline function is how static variable is used. We give an example as follows:

#include <stdio.h>
#define COUNT ({        \
  static int a = 0;     \
  printf("%d\n", ++a);  \
})

void count() __attribute__((always_inline));

void inline count() {
  static int a = 0;
  printf("%d\n", ++a);
}

int main() {
  printf("--- macro ---\n");
  COUNT;
  COUNT;
  COUNT;

  printf("--- inline ---\n");
  count();
  count();
  count();

  return 0;
}

output:

--- macro ---
1
1
1
--- inline ---
1
2
3

Comparison

  • Macros are not type safe itself.
  • Macros may result unexpected result if it's not defined well.
  • You cannot step through a #define in the debugger, but you can step through an inline function.
  • Macros are more flexible because they don't do type-checking and they can embed other macros.
  • Inline functions are not always guaranteed to be inlined. Some compiler needs to do extra configuration, or works only in release build.
    • Recursive functions is an example where most compilers ignore to inline
    • If you use -Os to make GCC/G++ optimize for minimum size, then inline functions may be ignored.
    • GCC/G++ will try to inline most possible functions if -O2 or -O3 is used for maximum speed.
  • Inline functions can provide scope for variables.
    • While preprocessor macros can use code blocks {...} to achieve this.
    • However, the static variables will not behave as your expectation in macros.
  • Macros can't access private or protected variables, while inline functions are possible.
  • In some case, it's not possible to be inlined.

See more discussion here

References

results matching ""

    No results matching ""