Macros that take many arguments
In Rust a repetition is what makes a macro variadic. The matcher writes $( ... ),* to accept zero or more comma-separated items, and the transcriber uses the same form to expand one piece of code per item. The separator after the closing parenthesis may be any token, and a trailing comma can be allowed with a second pattern.
This is the macro feature that replaces a variadic template or a hand-written overload set. The lesson builds a sum, a counter, a printer and a map literal, and ends with the error for two repetitions that do not have the same length.
A repetition
sum! takes zero or more expressions separated by commas, adds each one to a mutable total and returns it. The zero case has its own rule, because the repeated pattern with a star also accepts nothing but the block would then be a needless mut.
The program calls the macro with four numbers and with none. The two outputs are ten and zero.
macro_rules! sum {
() => {
0
};
($($value:expr),*) => {{
let mut total = 0;
$(total += $value;)*
total
}};
}
fn main() {
println!("{}", sum!(1, 2, 3, 4));
println!("{}", sum!());
}
rustc --edition 2024 sum.rs
./sum
10
0
The star means zero or more; a plus would mean one or more. The separator is repeated between the items, not after the last one, which is why a trailing comma needs its own rule. A repetition may also be written without a separator at all, which is how a macro accepts a run of tokens such as a type and its bounds. The parentheses around the group are required, and the parentheses around each expansion are the block the code lives in.
Counting arguments
A macro can recurse on its own input: one rule takes the first expression and the rest, and another rule stops at the empty list. The counter below returns the length of the list at compile time, because the recursion is expanded before the program exists.
The program counts three names and the empty list. The two outputs are three and zero.
macro_rules! count {
() => { 0 };
($head:expr $(, $tail:expr)*) => { 1 + count!($($tail),*) };
}
fn main() {
println!("{}", count!(a, b, c));
println!("{}", count!());
}
rustc --edition 2024 count.rs
./count
3
0
The recursive form is the general way to process a list when the expansion needs the position or a different shape per item. A plain repetition is shorter when every item is treated the same way.
Printing several values
print_all! expands one println! per expression, so a mixed list of an integer, a string and a float prints on three lines. Each expression keeps its own type, which a homogeneous container could not do.
The program prints the three values. The output is one line per value, in the order of the arguments.
macro_rules! print_all {
($($value:expr),*) => {
$(println!("{}", $value);)*
};
}
fn main() {
print_all!(1, "two", 3.5);
}
rustc --edition 2024 print_all.rs
./print_all
1
two
3.5
The same macro with print! and a separator would print one line. The C++ version of this program needs a variadic template and a recursion with a base case, which the comparison page writes.
Keys and values
A matcher may repeat a pair: $($key:expr => $value:expr),* accepts a list of key-value pairs, and the transcriber inserts both inside one repeated statement. The optional trailing comma is the rule $(,)?.
The program builds a map with two pairs and a trailing comma, then prints its length and one value. The macro is the shape the standard vec! and format! macros use.
macro_rules! map_of {
($($key:expr => $value:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $value);)*
map
}};
}
fn main() {
let map = map_of!("one" => 1, "two" => 2,);
println!("{} {}", map.len(), map["one"]);
}
rustc --edition 2024 named.rs
./named
2 1
The pair matcher is the one place where a repetition contains two metavariables, and they must repeat together. That is why the two are written inside one group and why the optional comma is a separate repetition of its own. The map is built at run time, one insert per pair, so the macro buys syntax rather than speed. The lesson on HashMap builds the same values without a macro.
Two repetitions that disagree
When a transcriber repeats two metavariables in one loop, their repetitions must have the same length. Two separate lists of different sizes are an error, and the message counts both sides, which is the clearest diagnostic a macro produces.
The macro below pairs left and right values, and the call gives two lefts and one right. The compiler reports the two counts and points at the transcriber.
macro_rules! pair_up {
($($left:expr),* ; $($right:expr),*) => {
$(println!("{} {}", $left, $right);)*
};
}
fn main() {
pair_up!(1, 2 ; "a");
}
rustc --edition 2024 repeat_err.rs
error: meta-variable `left` repeats 2 times, but `right` repeats 1 time
--> repeat_err.rs:3:10
|
3 | $(println!("{} {}", $left, $right);)*
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 1 previous error
The fix is one list of pairs, as the map macro does, or two separate loops that do not share a repetition. The error is the macro system checking the shape before any code is generated.
Exercises
- Write a macro that returns the largest of its arguments using two expressions at a time. Which repetition does it need?
- Change the printer to separate the values with commas on one line.
- Extend the map macro to accept a trailing comma and check both calls.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
$( ... ),*mean in a matcher?Answer
Zero or more items separated by commas
The star repeats the group zero or more times, and the comma is the separator.
-
What does a repetition in the transcriber do?
Answer
It expands the group once per matched item
The transcriber writes one copy of its group for each repetition of the metavariables.
-
How is an optional trailing comma allowed?
Answer
With a rule ending in $(,)?
The optional repetition of a comma accepts zero or one trailing comma.
-
What happens when two repeated metavariables have different lengths?
Answer
The compiler reports the two counts and stops
A repetition must have one length, so the macro system checks both sides.
-
What does this program print?
macro_rules! sum { () => { 0 }; ($($value:expr),*) => {{ let mut total = 0; $(total += $value;)* total }}; } fn main() { println!("{}", sum!(1, 2, 3, 4)); println!("{}", sum!()); }Answer
10 / 0
The four numbers add to ten, and the empty call takes the zero rule.
-
What does this program print?
macro_rules! count { () => { 0 }; ($head:expr $(, $tail:expr)*) => { 1 + count!($($tail),*) }; } fn main() { println!("{}", count!(a, b, c)); println!("{}", count!()); }Answer
3 / 0
The recursion adds one per item and stops at the empty rule, so three items count three.
-
What does this program print?
macro_rules! print_all { ($($value:expr),*) => { $(println!("{}", $value);)* }; } fn main() { print_all!(1, "two", 3.5); }Answer
1 / two / 3.5
One print per expression gives three lines, and each expression keeps its own type.
-
What does this program print?
macro_rules! map_of { ($($key:expr => $value:expr),* $(,)?) => {{ let mut map = std::collections::HashMap::new(); $(map.insert($key, $value);)* map }}; } fn main() { let map = map_of!("one" => 1, "two" => 2,); println!("{} {}", map.len(), map["one"]); }Answer
2 1
The optional comma rule accepts the last comma, and the map has two entries.
-
What is the first line of the compiler output for this program?
macro_rules! pair_up { ($($left:expr),* ; $($right:expr),*) => { $(println!("{} {}", $left, $right);)* }; } fn main() { pair_up!(1, 2 ; "a"); }Answer
error: meta-variable
leftrepeats 2 times, butrightrepeats 1 timeThe transcriber repeats both lists in one loop, and the counts do not match.
-
What does this program print?
macro_rules! first { ($head:expr $(, $rest:expr)*) => { $head }; } fn main() { println!("{}", first!(10, 20, 30)); }Answer
10
The rule binds the first expression as head and ignores the rest, and prints ten.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev