Macros#
Rust has two kinds of macros: declarative macros (macro_rules!) for
pattern-based code generation, and procedural macros for AST-level
transformations at compile time. Both are more powerful and safer than
C/C++ preprocessor macros.
Built-in Attributes#
- Source:
Attributes are metadata applied to items using #[...]. They control
compilation, generate code, or suppress warnings.
C++:
// Conditional compilation
#ifdef _WIN32
void platform() { std::cout << "windows"; }
#else
void platform() { std::cout << "other"; }
#endif
// Suppress warnings
#pragma warning(disable: 4101) // MSVC
[[maybe_unused]] int x = 5; // C++17
// Inline hint
inline int add(int a, int b) { return a + b; }
Rust:
// Conditional compilation
#[cfg(target_os = "windows")]
fn platform() -> &'static str { "windows" }
#[cfg(not(target_os = "windows"))]
fn platform() -> &'static str { "other" }
// Suppress warnings
#[allow(dead_code)]
fn unused_function() -> i32 { 42 }
// Inline hint
#[inline]
fn add(a: i32, b: i32) -> i32 { a + b }
Common built-in attributes:
#[derive(Debug, Clone, PartialEq)] // auto-generate trait impls
#[cfg(test)] // only compile in test builds
#[test] // mark as test function
#[allow(unused_variables)] // suppress warning
#[inline] // inline hint
#[must_use] // warn if return value is ignored
Derive Macros#
#[derive] auto-generates trait implementations. This has no C++ equivalent —
C++ requires manually writing operators, copy constructors, etc.
C++ (manual implementation):
#include <iostream>
struct Point {
double x, y;
// Must manually implement operator==
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
// Must manually implement operator<< for printing
friend std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "Point { x: " << p.x << ", y: " << p.y << " }";
}
// Copy constructor/assignment generated by default in C++
};
Rust (auto-generated with derive):
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
let p2 = p.clone(); // Clone
println!("{:?}", p); // Debug
assert_eq!(p, p2); // PartialEq
}
Common derivable traits:
#[derive(
Debug, // {:?} formatting
Clone, // .clone() method
Copy, // implicit copy (like C++ trivial types)
PartialEq, // == and !=
Eq, // stricter equality (no NaN issues)
Hash, // for use in HashMap keys
Default, // Default::default() with zero/empty values
)]
Declarative Macros (macro_rules!)#
- Source:
Declarative macros use pattern matching to generate code. Unlike C++ #define,
they are hygienic (no name collisions) and type-aware.
C++ (preprocessor macros):
// Simple macro - text substitution, no type safety
#define SAY_HELLO() std::cout << "Hello!" << std::endl
// Macro with argument - prone to double evaluation bugs
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// Variadic macro
#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)
Rust (declarative macros):
// Simple macro
macro_rules! say_hello {
() => {
println!("Hello!");
};
($name:expr) => {
println!("Hello, {}!", $name);
};
}
say_hello!(); // Hello!
say_hello!("Rust"); // Hello, Rust!
Repetition patterns — no C++ equivalent:
// $(...),* matches zero or more comma-separated items
macro_rules! vec_of_strings {
($($x:expr),* $(,)?) => {
vec![$($x.to_string()),*]
};
}
let v = vec_of_strings!["a", "b", "c"];
// expands to: vec!["a".to_string(), "b".to_string(), "c".to_string()]
Generating functions with macros:
macro_rules! make_adder {
($name:ident, $t:ty) => {
fn $name(a: $t, b: $t) -> $t {
a + b
}
};
}
make_adder!(add_i32, i32);
make_adder!(add_f64, f64);
assert_eq!(add_i32(1, 2), 3);
assert_eq!(add_f64(1.0, 2.0), 4.0);
Macro fragment types:
// $x:expr - expression (1 + 2, foo(), "hello")
// $x:ident - identifier (variable/function name)
// $x:ty - type (i32, String, Vec<u8>)
// $x:pat - pattern (Some(x), (a, b))
// $x:stmt - statement (let x = 1;)
// $x:block - block ({ ... })
// $x:tt - single token tree (any token)
Procedural Macros#
Procedural macros operate on the token stream (AST) at compile time. They are
more powerful than macro_rules! but must be defined in a separate crate
with proc-macro = true.
There are three types:
// 1. Attribute macros — #[something]
// Transforms the annotated item
#[proc_macro_attribute]
pub fn my_attr(args: TokenStream, item: TokenStream) -> TokenStream { ... }
// Usage: #[my_attr]
// 2. Derive macros — #[derive(Something)]
// Adds code alongside the annotated item
#[proc_macro_derive(MyTrait)]
pub fn my_derive(input: TokenStream) -> TokenStream { ... }
// Usage: #[derive(MyTrait)]
// 3. Function-like macros — something!(...)
// Called like a function
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { ... }
// Usage: sql!(SELECT * FROM users)
Crate setup for procedural macros:
# Cargo.toml for a proc macro crate
[lib]
proc-macro = true
[dependencies]
syn = "2" # parse Rust syntax
quote = "1" # generate Rust code
proc-macro2 = "1" # TokenStream utilities
Real-world example — #[tokio::main] expands async main:
// What you write:
#[tokio::main]
async fn main() {
do_stuff().await;
}
// What it expands to:
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
do_stuff().await;
});
}
C++ has no equivalent to procedural macros. The C preprocessor does text substitution, while Rust proc macros parse and transform actual syntax trees at compile time.
Comparison Summary#
Feature |
Rust |
C++ |
|---|---|---|
Simple macros |
|
|
Conditional compilation |
|
|
Auto-generate code |
|
No equivalent |
AST transformation |
Procedural macros |
No equivalent |
Suppress warnings |
|
|
Inline hint |
|
|
See Also#
Rust Basics - Variables, mutability, underscore placeholder
Traits and Generics - Traits that can be derived