Skip to content
📖 Auto vs Modern CBook contents

Chapter 17: Function-like Macros ​

Level 3 -- Experience

Text substitution, preprocessor tricks, and why Auto replaces macros with compile-time metaprogramming.

Function-like macros are one of C's most powerful and most dangerous features. They enable metaprogramming through text substitution before the compiler sees the code. Auto takes a fundamentally different approach: it provides compile-time code generation through the #[] comptime system, which is type-safe and predictable.


17.1 How Macros Work ​

Macros are processed by the C preprocessor before compilation. The preprocessor performs text substitution:

c
// C Deep Dive: macro basics
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x)  ((x) * (x))
#define PI 3.14159265

int result = MAX(3, 7);        // expands to: ((3) > (7) ? (3) : (7))
int area = SQUARE(5);          // expands to: ((5) * (5))
double circ = 2 * PI * r;     // expands to: 2 * 3.14159265 * r

The preprocessor is a separate phase of translation. It knows nothing about C types, scoping rules, or syntax. It performs literal text replacement.

Macro expansion happens in several steps:

  1. Scan the token sequence for macro invocations.
  2. Substitute arguments (with # and ## processing).
  3. Rescan the result for further macros.
  4. Repeat until no more substitutions are possible.

C Deep Dive: The rescanning step can cause infinite recursion. The C standard prevents this by not re-expanding a macro during its own expansion. But mutual recursion between macros is allowed and is sometimes used intentionally in advanced macro programming.

Auto's approach. Auto has no preprocessor and no macros. Every operation that would use a macro in C uses a regular function or comptime metaprogramming:

auto
// Auto: regular functions replace macros
fn safe_max(a int, b int) int {
    if a > b { a } else { b }
}

fn safe_square(n int) int {
    n * n
}

17.2 Argument Checking ​

Macros do not check argument types. The substitution is purely textual, which leads to subtle bugs:

c
// C Deep Dive: macro pitfalls
#define SQUARE(x) ((x) * (x))

int a = SQUARE(2 + 3);
// Expands to: ((2 + 3) * (2 + 3)) = 25 (correct with parentheses)

// But what about this?
#define BAD_SQUARE(x) x * x
int b = BAD_SQUARE(2 + 3);
// Expands to: 2 + 3 * 2 + 3 = 2 + 6 + 3 = 11 (wrong!)

The most dangerous pitfall is double evaluation:

c
// C Deep Dive: double evaluation
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = MAX(x++, 3);
// Expands to: ((x++) > (3) ? (x++) : (3))
// x is incremented TWICE if x > 3! x becomes 7, not 6.

Common macro problems:

ProblemExampleConsequence
Missing parenthesesx * x with 2 + 3Wrong precedence
Double evaluationMAX(x++, y)Side effect runs twice
No type checkingMAX("hello", 3)Compiles, crashes
No debuggingStep into macroNot visible in debugger
Scope issuesMacro uses local variableName collisions

C Deep Dive: The rule for safe macros: always parenthesize every parameter and the entire expression. #define SQUARE(x) ((x) * (x)). Even then, double evaluation is impossible to prevent. The only truly safe macro is one whose arguments are simple identifiers or constants.

Auto's approach. Auto functions have none of these problems:

auto
// Auto: no macro pitfalls
fn safe_max(a int, b int) int {
    if a > b { a } else { b }
}

fn main() {
    // Arguments evaluated exactly once
    var x int = 5
    let y int = safe_max(x, 3)   // x evaluated once
    print("x:", x)               // x is still 5
    print("y:", y)               // y is 5
}

Released under the MIT License.