TL;DR
- 宣言的マクロは定義より後ろでしか使えず、
#[macro_export]を付けるとクレートのトップレベルに現れる。 - マクロの挙動は、できるだけ通常の Rust と一致させる。制御フローを外に漏らさず、引数は 1 度だけ評価する。
- 引数をフォーマットして出力するマクロでは
format_args!を使う。
内容
項目28 分別をもってマクロを使おうのうち、宣言的マクロを使うときの注意点である。
可視範囲
マクロの定義を使えるのは、その定義より後ろのコードだけである。
fn before() {
println!("square {} is {}", 2, square!(2)); // コンパイルエラー
}
macro_rules! square {
{ $e:expr } => { $e * $e }
}#[macro_export] を付けると可視範囲を広げられる。ただし、モジュールの中で定義していても、マクロはクレートのトップレベルに現れる。
mod submod {
#[macro_export]
macro_rules! cube {
{ $e:expr } => { $e * $e * $e }
}
}
let cubed = crate::cube!(3); // crate::submod::cube! ではない健全性
宣言的マクロは健全なマクロである。展開された本体から、呼び出し側のローカル変数は使えない。
macro_rules! increment_x {
{} => { x += 1 };
}
let mut x = 2;
increment_x!(); // コンパイルエラー一方、引数として渡されたものに対しては、生成されたコードの中で何でもできる。参照を通さず、所有している値を直接書き換えることもできる。マクロ名に警告の意味で ! が付くのはこのためである。展開後のコードは cargo-expand で確かめられる。
制御フローを漏らさない
生成されたコードの中では、ループ、条件分岐、return、? などの制御フローも使える。しかし、呼び出し側から見て意図せず関数を抜けるのは、驚き最小の原則に反する。マクロの挙動は通常の Rust と一致させよう。
macro_rules! check_successful {
{ $e:expr } => {
if $e.group() != Group::Successful {
return Err(MyError("HTTP operation failed"));
}
}
}この場合は、マクロに Result を返させれば、呼び出し側で check_successful!(rc)?; と書けて制御フローが明確になる。マクロの目的が Rust と違う挙動をさせることなら、この限りではない。ただし、その違いを文書にして伝える。
副作用のある引数
引数を複数回使うマクロに副作用のある式を渡すと、予期しない結果になる。
let mut x = 1;
let y = square!({
x += 1;
x
});
println!("x = {x}, y = {y}"); // x = 2, y = 4 ではなく x = 3, y = 6対策は 2 つある。式を 1 度だけ評価してローカル変数に入れるか、フラグメント指定子 expr を ident に変えて、そもそも式を受け付けないようにする。
format_args! を使う
引数をフォーマットして出力するマクロでは、format_args! を使おう。println! や format! と同じフォーマット構文を使え、フォーマット引数がコンパイル時に検査されるので型安全である。
macro_rules! my_log {
{ $($arg:tt)+ } => {
eprintln!("{}:{}: {}", file!(), line!(), format_args!($($arg)+));
}
}
let x = 10u8;
my_log!("x = {:#04x}", x); // src/main.rs:9: x = 0x0a