Macros

Macros in C/C++ are basically little rules that are defined by a preprocessor and substituted into the code that the compiler ultimately attempts to compile.

Modern coding practice these days is to use inline functions and constants instead of macros.

But the reality is they can still be (ab)used and code often does. For example code might insert debug statements or logging which is compiled away in release mode.

Another common use is on Windows where the type TCHAR compiles to be either char or wchar_t depending on #define UNICODE being present or not. Along with it go macros like USES_CONVERSION, A2CT, T2CW etc. Code should compile cleanly either way but the reality is usually it doesn’t.

A classic problem would be something like this:

  1. #define SQUARED(x) x * x
  2. // And in code
  3. float result = SQUARED(++x);
  4. That would expand to
  5. float result = ++x * ++x;

So the value in result would be wrong and the value in x would be incremented twice.

Compilation errors

Consider we are compiling this structure:

  1. // Header
  2. struct Tooltip
  3. #if TOOLTIP_VERSION > 4
  4. char buffer[128];
  5. #else
  6. char buffer[64];
  7. #endif
  8. };

And in C++

  1. Tooltip tooltip;
  2. memset(&tooltip, 0, sizeof(tooltip));

If we fail to define TOOLTIP_VERSION to the same value in the implementation as in the caller, then this code may stomp all over memory because it thinks the struct is 128 bytes in one place and 64 bytes in another.

Namespace issues

Macros aren’t namespaced and in some cases this leads to problems where a macro definition collides with a well qualified symbol.
For example code that #include <windows.h> gets a #define TRUE 1. But that excludes any other code that expects to compile on Windows from ever using TRUE as a const no matter how well they qualify it. Consequently code has to do workarounds such as #undef macros to make code work or using another value.

  1. #ifdef TRUE
  2. #define TMP_TRUE TRUE
  3. #undef TRUE
  4. #endif
  5. bool value = myapp::TRUE;
  6. #ifdef TMP_TRUE
  7. #define TRUE TMP_TRUE
  8. #undef TMP_TRUE
  9. #endif

Ugh. But more likely we’ll rename myapp::TRUE to something like myapp::MYAPP_TRUE to avoid the conflict. It’s still an ugly workaround for a problem caused by inconsiderate use of macros.

Commonly used words like TRUE, FALSE, ERROR, OK, SUCCESS, FAIL are more or less unusable thanks to macros.

How Rust helps

Rust provides developers with consts, inline attributes, and platform / architecture attributes for the purpose of conditional compilation.

Rust offers macros but they consist of a set of matching rules than must generate syntactically Rust. Macro expansion is performed by the compiler so it is capable of generating errors on the macro if the macro is in error.