Macros are great; ffs stop using macros.

Macros are awesome. Macros are powerful. Macros will single-handedly turn your simple, readable codebase into an incomprehensible Lovecraftian nightmare of arcane symbols and unpredictable behavior. And yet, every day, developers continue to reach for them like a moth to a flame.

So, what’s the deal? Why are macros both a godsend and a cursed artifact? And why, for the love of all that is good and holy, should you stop using them in most cases?

You know the drill. Take a moment before scrolling down.

Wtf are macros?

At their core, macros are a way to tell the compiler (or preprocessor, depending on the language) to replace one piece of code with another—usually at compile time. This can mean anything from simple text replacement (#define in C) to full-on metaprogramming sorcery (Rust’s procedural macros, Lisp’s everything).

Macros can be used to:

  • Generate repetitive boilerplate
  • Optimize performance by inlining code
  • Perform compile-time computations
  • Completely wreck your debugging experience and make outages a living hell.

Macros are great! (Sort of.)

Macros exist because they solve real problems. Let’s take an example in Rust:

macro_rules! make_struct {
    ($name:ident) => {
        struct $name {
            data: i32,
        }
    };
}

make_struct!(Foo);
make_struct!(Bar);

Boom! We just created two structs without writing the same code twice. Feels great, right?

Or in C:

#define SQUARE(x) ((x) * (x))

Nice! We don’t have to write a function for squaring a number. More efficient, right?

But then, some poor soul writes this:

int result = SQUARE(5 + 2);

And the compiler happily expands it to:

int result = (5 + 2 * 5 + 2);

Which evaluates to 14, not 49. Congratulations, you have just entered Macro Hell™.

The dark side of macros

While macros give you power, they come with some very nasty trade-offs:

1. Debugging macros is pain incarnate

Ever tried stepping through a macro in a debugger? It’s like trying to read a book that’s being rewritten while you turn the pages. Since macros operate before actual compilation, they don’t exist in your final code in a way that debuggers can follow.

2. Macros break syntax highlighting and tooling

A good IDE can handle functions, classes, and modules. But when you start using macros, all bets are off. Code completion stops working, error messages become cryptic, and syntax highlighting goes on strike.

3. Macros introduce hidden complexity

What looks like a single macro call could expand into a monstrous web of conditional branches and recursive templates. Your future self (or your teammates) will curse you when they have to decipher the spaghetti code generated by an innocent-looking macro.

4. Better alternatives exist

Most modern languages offer built-in solutions that achieve the same goals without the pain of macros:

  • Inline functions (C++, Rust, etc.)
  • Generics and templates (C++, Rust)
  • Metaprogramming tools (Python decorators, Rust’s derive, etc.)
  • Prebuilt utilities (Why write a macro when a standard library function exists?)

When should you actually use macros?

Let’s be fair—there are cases where macros are the right tool for the job:

  • Domain-Specific Languages (DSLs): Rust’s tokio::main macro, Lisp’s everything.
  • Compile-time computations: If your language lacks constexpr-style evaluation.
  • When no other alternative exists: Some low-level performance-critical cases.

But for everyday coding? Macros are overkill. Just use functions, generics, or templates instead.

Conclusion

Macros are powerful, but with great power comes great unreadability. Use them only when necessary, and for everything else, let the compiler do the work with more maintainable alternatives.

If you must use macros, at least document them properly, or risk summoning eldritch horrors into your codebase.

Further readings

  • Hygienic Macros in Rust
  • C Preprocessor Nightmares
  • How Lisp Macros Work
  • Template Metaprogramming in C++ (A Cautionary Tale)
Essam Hassan

Essam Hassan

A pragmatic software engineer, cyber security enthusiast and a Linux geek. I curse at my machine on a daily basis. My views are my own.
Zurich, Switzerland