How Rust macros differ from C/C++ macros
At a high level, you can think of Rust macros as an improved version of C-style macros, one that fixes many of the classic problems in C while also adding some genuinely new capabilities.
In C, a macro is something only slightly more advanced than copy-paste - and sometimes not by much. #include, for example, recursively copies another file into the current file. #define replaces a placeholder with some text. It works, but the model feels a bit like simulating async with sleep: useful in practice, yet nowhere near as precise as a real language construct.
That rough design leads to results that often violate first intuition. The classic unexpected macro behavior example looks like this:
#define FIVE_TIMES(x) 5 * x
int main() {
printf("%d\n", FIVE_TIMES(2 + 3));
return 0;
}Because the macro is only doing textual substitution, the expression becomes 5 * 2 + 3, which evaluates to 13 instead of the expected 25.
Rust improves this in a lot of ways. One of the classic descriptions is that Rust macros are hygienic. The Rust docs cover that idea separately in their section on hygiene. They summarize the design like this:
Each macro expansion happens in a distinct ‘syntax context’, and each variable is tagged with the syntax context where it was introduced.
If you reimplemented the C example above as a Rust macro, then 2 + 3 inside the macro invocation would be treated as a single expression, not as raw text to be blindly pasted in.
Rust macros also survive as part of distributed crates instead of disappearing after a preprocessing pass, and Rust supports multiple macro styles such as attribute-like macros and function-like macros. They also support variadic-style patterns, something Rust functions still do not support directly.
macro vs function
Like in C/C++, a macro is checked and expanded at compile time.
Rust takes advantage of that compile-time expansion to eliminate some classic safety problems. A pwn player can exploit a format-string vulnerability in their sleep, and the reason printf-style bugs happen is that the string is constructed at runtime without sufficiently strict argument checking. Rust’s print! macro avoids that. Because it expands at compile time, the compiler can already verify whether the argument count matches, whether the variables are valid, whether the first argument is a legal string literal, and so on. That allows Rust to remove an entire class of format-string hazards up front.
A function, by contrast, works much more like functions in other languages. Rust functions do come with a few extra restrictions: no variadic parameters, no function overloading (operators can still be overloaded), no default argument values, and so on. The official philosophy is basically that Rust can still express those use cases in other clean ways without leaning on features that often make programs harder to reason about.
One major difference between macro and function is that a macro does not evaluate its arguments ahead of time-or rather, the passed expression is only evaluated where the expanded code actually uses it.
That wording can sound a bit abstract, so here is a simpler way to think about it.
Suppose you call a function like this: hello(world(123)). The program first evaluates world(123), and only then passes the resulting value into hello. That first step is evaluation. In a macro, the argument is passed in as an expression entity. Before the expanded macro body uses it, that expression has not actually been executed yet.
So when should you use a macro, and when should you use a function?
From the comparison above, the most distinctive property of a macro is that it puts far fewer restrictions on the incoming argument and does not force the expression to be evaluated before expansion.
First, there are cases where you more or less have to use a macro. The standard assert! macro is a good example:
#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
macro_rules! assert {
($cond:expr) => (
if !$cond {
panic!(concat!("assertion failed: ", stringify!($cond)))
}
);
($cond:expr, $($arg:tt)+) => (
if !$cond {
panic!($($arg)+)
}
);
}If you tried to write the same thing as a function, it would look something like this:
fn assert(cond: bool, expr: ?) {
...
}Clearly there is no sensible type you can put on expr here. And since expr would be evaluated before being passed in, what you receive is just a value, not the original expression. That means you cannot do something like stringify!($cond) inside a normal function.
The same logic applies to things like printf-style formatting, other operations that need compile-time safety checks, or any API that effectively wants variadic arguments. Those are all good candidates for macros.
Macros also have one more advantage: some work inside them can be done at compile time. That can enable compiler-generated lookup-table-style code and significantly reduce runtime cost in the right situation.
The general rule of thumb is simple: use a macro when a function cannot provide the behavior you need, when you are dealing with highly repetitive structural patterns, or when you need to inspect syntax and generate code at compile time. In every situation where a function is good enough, prefer the function.
Because macros impose fewer constraints on inputs, overusing them can make a codebase much harder to maintain and much harder to read. And since macros expand at compile time, they can also duplicate a lot of code into the final binary, which may hurt instruction-cache behavior and end up reducing performance.
References
What is the difference between macros and functions in Rust? - StackOverflow
