Macros and templates: C++ and Rust
A variadic template and a macro_rules! macro both accept a list of arguments of any length. The C++ version is a recursion over types, checked when it is instantiated; the Rust version is a pattern over tokens, checked when the macro is called. The two programs print the same list and fail in different places.
The task in C++
print_all takes one value and a pack, prints the first and recurses on the rest. The empty overload ends the recursion. Each call is an ordinary function call, and the compiler instantiates the template for the exact list of types.
The program prints an integer, a string and a double on one line separated by spaces, then an empty line. The types do not need to be related in any way.
#include <iostream>
#include <string>
void print_all()
{
std::cout << '\n';
}
template <typename T, typename... Rest>
void print_all(const T& first, const Rest&... rest)
{
std::cout << first << ' ';
print_all(rest...);
}
int main()
{
print_all(1, std::string("two"), 3.5);
print_all();
}
g++ -std=c++23 -Wall -Wextra macros.cpp -o macros
./macros
1 two 3.5
The recursion is resolved at compile time, and the binary contains one call per value. The empty overload is the base case, without which the pack version would not compile for an empty list.
The same task in Rust
The macro has two rules: the empty call prints a newline, and the general rule repeats println! once per expression. There is no recursion and no base overload beyond the first rule, because a repetition may match zero items.
The program prints the three values on three lines and the empty call prints nothing visible. Each expression keeps its own type, so the macro is as flexible as the template.
macro_rules! print_all {
() => {
println!()
};
($($value:expr),* $(,)?) => {
$(println!("{}", $value);)*
};
}
fn main() {
print_all!(1, "two", 3.5);
print_all!();
}
rustc --edition 2024 macros.rs
./macros
1
two
3.5
The expansion is a sequence of statements, so there is no recursion to follow and no type to name. The lesson on macro repetition builds this macro step by step.
What differs and why
Who owns the values: the C++ version takes const references and copies nothing, and the Rust macro passes each expression to println! by reference as well. Neither allocates for the list itself, and both keep the original values alive.
What the compiler rejects: a C++ template accepts any list that the body can handle and reports errors deep inside the instantiation. A Rust macro has an explicit matcher, so a call with the wrong shape is rejected at the call site with the token that did not fit.
The Rust macro also has hygiene, which the template does not: a name introduced inside the expansion cannot collide with a name at the call site, and the caller cannot see it. A C++ macro would textually substitute tokens instead, which is why macros in that language need parentheses around every argument. What happens at run time: the same lines in a different layout. The C++ version puts the values on one line, the Rust one on separate lines, because the macro decides the layout rather than the stream.
The habit that breaks
The habit is to assume a macro is as forgiving as a template. A template with a pack accepts any number of arguments; a macro accepts exactly the shapes its rules describe. Calling a one-argument macro with two values is a build error, and the message names the token that did not fit.
macro_rules! print_one {
($value:expr) => {
println!("{}", $value)
};
}
fn main() {
print_one!(1, 2);
}
rustc --edition 2024 macros_wrong.rs
error: no rules expected `,`
--> macros_wrong.rs:8:17
|
1 | macro_rules! print_one {
| ---------------------- when calling this macro
...
8 | print_one!(1, 2);
| ^ no rules expected this token in macro call
|
note: while trying to match meta-variable `$value:expr`
--> macros_wrong.rs:2:6
|
2 | ($value:expr) => {
| ^^^^^^^^^^^
error: aborting due to 1 previous error
The fix is the repetition from the main program: $($value:expr),* accepts the list, and the transcriber prints one line per item. The C++ lesson for this ground is Variadic templates, and the Rust lesson is Macros with macro_rules!.
The C++ program was built with g++ 15.2.0 and the Rust program with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev