Functions should always be preferred over macros, except when macros provide essential functionality that functions cannot, such as variadic interfaces, compile-time code generation, or syntax extensions via custom derive and attribute macros.
|
|
Macros are powerful but they come at the cost of readability, complexity, and maintainability. They obfuscate control flow and type signatures.
Debugging Complexity
Optimization
Macros may inhibit compiler optimizations that work better with functions.
Macros act like #[inline(always)] functions, which can lead to code bloat.
They don’t benefit from the compiler’s inlining heuristics, missing out on selective inlining where the compiler decides when inlining is beneficial.
Functions provide
|
|
|
Using a macro where a simple function would suffice, leads to hidden mutation:
macro_rules! increment_and_double {
($x:expr) => {
{
$x += 1; // mutation is implicit
$x * 2
}
};
}
let mut num = 5;
let result = increment_and_double!(num);
println!("Result: {}, Num: {}", result, num);
// Result: 12, Num: 6
In this example, calling the macro both increments and returns the value in one go—without any clear indication in its “signature” that it mutates its argument. As a result, num is changed behind the scenes, which can surprise readers and make debugging more difficult.
|
|
|
The same functionality, implemented as a function with explicit borrowing:
fn increment_and_double(x: &mut i32) -> i32 {
*x += 1; // mutation is explicit
*x * 2
}
let mut num = 5;
let result = increment_and_double(&mut num);
println!("Result: {}, Num: {}", result, num);
// Result: 12, Num: 6
The function version makes the mutation and borrowing explicit in its signature, improving readability, safety, and debuggability.
|
|