Skip to content

guideline: a macro should not be used in place of a function #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 17, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion src/coding-guidelines/macros.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,89 @@ Macros

// TODO

.. guideline:: A macro should not be used in place of a function
:id: gui_2jjWUoF1teOY
:category: mandatory
:status: draft
:release: todo
:fls: fls_xa7lp0zg1ol2
:decidability: decidable
:scope: system
:tags: reduce-human-error

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.

|

.. rationale::
:id: rat_M9bp23ctkzQ7
:status: draft

Macros are powerful but they come at the cost of readability, complexity, and maintainability. They obfuscate control flow and type signatures.

**Debugging Complexity**

- Errors point to expanded code rather than source locations, making it difficult to trace compile-time errors back to the original macro invocation.

**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**

- Clear type signatures.
- Predictable behavior.
- Proper stack traces.
- Consistent optimization opportunities.


.. non_compliant_example::
:id: non_compl_ex_TZgk2vG42t2r
:status: draft

Using a macro where a simple function would suffice, leads to hidden mutation:

.. code-block:: rust

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.


.. compliant_example::
:id: compl_ex_iPTgzrvO7qr3
:status: draft

The same functionality, implemented as a function with explicit borrowing:

.. code-block:: rust

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.



.. guideline:: Shall not use Function-like Macros
:id: gui_WJlWqgIxmE8P
:category: mandatory
Expand Down Expand Up @@ -342,4 +425,4 @@ Macros

fn example_function() {
// Compliant implementation
}
}