24.xChapter 24 summary and quiz

Last updated June 13, 2026

A short chapter on the part of Rust that writes Rust. Review, then a quiz.

Quick review

A macro is code that writes code at compile time: it takes your source and expands it into more source (24.1). That's the ! you've typed since lesson 1.1, "this is expanded, not called." Macros do what functions can't: variable argument counts (println!), construction syntax (vec!), generating repetitive code.

Declarative macros are written with macro_rules!: a set of rules, each a matcher (left of =>) and an expansion (right). Capture input with metavariables like $x:expr (also ident, ty, literal, tt), and match "any number of" with repetition $( ... ),*, which vec! uses to emit one push per element. Macros are also hygienic: their internal identifiers can't collide with the caller's.

When to use one (24.2): ask "could this be a function?" first (usually yes, prefer it). If the variation is across types, use a generic (chapter 15). Reach for a macro only for what neither can do, variable arguments, generating repetitive code across types, new syntax, operating on code itself, because macros cost readability and have rougher errors.

Procedural macros are compile-time functions that manipulate code, more powerful than macro_rules!, in three kinds (24.3): derive (#[derive(Debug)], #[derive(Serialize)], generate code from a type's structure), attribute (#[tokio::main], transform a tagged item), and function-like (sqlx::query!, procedurally-powered name!(...)). The crates you use most (serde, Tokio) are essentially procedural macros; you'll use them constantly and write them rarely.

Quiz time

Question #1

What does the ! in println! actually signify, finally?

Show solution

It marks a macro invocation rather than a function call. The macro is expanded at compile time into real source code (the actual formatting-and-writing logic), which the compiler then compiles. That's why println! can take a variable number of arguments, something no function signature allows. The "supercharged function" from lesson 1.1 was a macro: code that writes code, expanded before compilation.

Question #2

For each, say whether you'd use a function, a generic, or a macro: (a) compute the average of a Vec<f64>; (b) find the max of a slice of any orderable type; (c) build a collection from a variable-length list of elements written inline.

Show solution

(a) Function, fixed arguments, fixed type, a plain computation. (b) Generic, the variation is across types, so a generic function with a PartialOrd bound (like largest<T: PartialOrd>) keeps full type checking. (c) Macro, a variable number of inline elements (like vec![...]) can't be a function signature, and the construction syntax is exactly what macros provide. The rule: fixed → function; varies by type → generic; varies by argument count or needs new syntax → macro.

Question #3

You write #[derive(Debug, Clone)] on a struct. What kind of macro is derive, and what does it do here?

Show solution

derive is a procedural macro (specifically a derive macro). At compile time it inspects the struct's structure (its fields and their types) and generates trait implementations: Debug produces a fmt that prints the fields, Clone produces a clone that clones each field. You wrote two words and the macros generated two full impl blocks of boilerplate, code tailored to your type, which is what makes procedural macros more powerful than pattern-matching macro_rules!.

Question #4

Why are Rust's macros being "hygienic" a meaningful safety improvement over C's preprocessor macros?

Show solution

Hygiene means identifiers a macro uses internally are kept separate from identifiers in the calling code, so they can't accidentally collide. In C, a macro that uses a variable named temp can silently clash with the caller's temp and corrupt it, a classic, hard-to-find bug. Rust's compiler keeps the two namespaces distinct automatically, so this whole class of name-collision bug can't happen, making macros far safer to use.

You now understand the whole macro system, from the ! in your first program to the #[derive]s that power the ecosystem, and the judgment of when code-generation is the right tool. Chapter 25 enters Rust's most dangerous territory: unsafe. For 24 chapters the compiler has guaranteed memory safety. Now you'll learn the small set of operations where it can't, why they exist, and how Rust contains them, so that "unsafe" means "a contract you uphold," not "safety off."