Defines Considered Harmful

Why bother

Macrodefinitions in C/C++ code are a sharp tool which must be wielded with care. Modern compilers allow us to write modern C code without resorting to the majority of preprocessor tricks we’ve had to use if we had been stuck in 1999. The goal here is, as always, to increase programmer’s productivity by eliminating small but annoying old problems and not allowing them to reappear in new code.

Without further dwelling into theory, let’s see its applicability for legacy project’s code base.

Keep in mind slight but annoying differences in how C and C++ language treat const. If you don’t know these difference, please take a minute to refresh your understanding [1] (slides 1 and 2).

When not to use defines

  1. To define an integer or string constant. Consider using const, static const, anonymous or named enum instead. In C, sometimes it is unavoidable to use a #define-constant, but these cases should be rare.

  2. To define a short named expression used in many places. Use inline or static inline function. The latter may even be placed in header files, just as it is with defines.

  3. To segregate code specific to a particular sub-project needs. Unless there are secrecy reasons (see below), use runtime measures to control absence of semantics. Use configuration knobs. Ideally, all code belonging to an sub-module should be kept in an isolated group (let’s call it folder?) of source files, and those could simply be excluded from a build at Makefile level when not needed.

What to do with existing defines

There are dauntingly many existing macrodefines which could have been regular C/C++-language constructs. Some of them are particularly nasty. Let’s not contribute to increasing their number without a reason.

  • Do not create new similarly-designed macro-constructs near existing ones just because the whole existing file uses them all over the place. Do not succumb to the weight of the theory of broken windows [5].
  • Do not expand existing macro definitions. If you need to modify them with new functionality, convert them first into a better form.

  • Whenever possible, convert a single old literal/”function” to a proper const/function.

Remember about the boyscout rule [4] when working on macrodefinitions.

When to use defines

  • As header include guards [6], extern "C" guards for shared C/C++ headers.

  • MODULE_XX_HAS_YYY — only for secrecy reasons. Any such macro should have a limited life time until related technology becomes public.

  • When there is source code text manipulation involved to perform something not achievable without source code manipulation. Examples are: ASSERT, STATIC_ASSERT, token concatenation [2] and stringizing [3] techniques.

  • To guard compiler/host/OS -specific code. E.g. layout of structures adjusted for big/little endianness may have sections guarded by defines.

  • When there is data-backed performance issue which requires precise control over generated code. To prove your point for using a macro, write a micro-benchmark to measure effects of using a define that shows statistically significant effects. Or better yet, demonstrate a measurable macroscopic effect with a full-blown benchmark.

References

  1. https://staff.tarleton.edu/agapie/documents/cosc2321/cosc_2321_ch04_data-abstraction.pdf
  2. https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html
  3. https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
  4. Always leave the campground cleaner than you found it.
  5. https://en.wikipedia.org/wiki/Broken_windows_theory
  6. https://en.wikipedia.org/wiki/Include_guard

Written by Grigory Rechistov in Uncategorized on 23.03.2020. Tags: refactoring, define, macrodefines,


Copyright © 2020 Grigory Rechistov