宣言的マクロを利用する上で注意すべきこと
マクロの定義を利用できるのは、その定義よりも後ろのコードのみ
e.g. コンパイルエラー
code:rs
fn before() {
println!("before square {} is {}", 2, square!(2)) // コンパイルエラー }
macro_rules! square {
{ $e:expr } => { $e * $e }
}
fn after() {
sprintln!("after square {} is {}", 2, square!(2)) }
#[macro_export] を付けると可視範囲を広げることができるが、モジュール 内で定義していたとしても、クレート のトップレベルに現れる e.g.
code:rs
mod submod {
macro_rules! cube {
{ $e:expr } => { $e * $e * $e }
}
}
mod user {
pub fn use_macro() {
let cubed = crate::cube!(3); // crate::submod::cube! ではない
println!("cube {} is {}", 3, cubed);
}
}
宣言的マクロは 健全なマクロ であり、本体が展開された際にはローカル変数を利用できない e.g.
code:rs
macro_rules! increment_x {
{} => { x += 1 };
}
定義だけでは問題ないが、利用しようとするとコンパイルエラーとなる
code:rs
let mut x = 2;
increment_x!();
println!("x = {}", x);
マクロで生成されたコード内では、引数に対して何でもできる
そのため、マクロ名には接尾辞として、警告を表す ! が付与される
e.g. (参照 を通じてではなく)所有権 を用いて更新する code:rs
struct Item {
contents: usize,
}
macro_rules! inc_item {
{ $x:ident } => { $x.contents += 1; }
}
let mut x = Item { contents: 42 };
inc_item!(x);
println!("x is {x:?}");
code:rs
let mut x = Item { contents: 42 };
x.contents += 1;
{ ::std::io::_print(format_args!("x is {0:?}\n", x)); };
生成されたコード内では、制御フロー操作(ループや条件分岐、return、? )を行うことができる
これは 驚き最小の原則 に反するため、可能な限り「マクロの挙動が通常の Rust と一致するようにしよう」 e.g. return 文を本体に持つマクロ
code:rs
macro_rules! check_successful {
{ $e:expr } => {
if $e.group() != Group::Successful {
return Err(myError("HTTP operation failed"));
}
}
}
呼び出し元のコードの制御フローは(意図せず関数から抜ける可能性があるので)不明瞭になる
code:rs
let rc = perform_http_operation();
check_successful!(rc);
解決策: Result を生成する
code:rs
macro_rules! check_successful {
{ $e:expr } => {
match $e.group() {
Group::Successful => Ok(()),
_ => Err(MyError("HTTP operation failed")),
}
}
}
これだと、呼び出し元のコードは明確になる
code:rs
let rc = perform_http_operation();
check_successful!(rc)?;
一方、マクロの目的が Rust とは異なる挙動にすることである場合は、この限りではない
ただし、ドキュメント化して通常の Rust とは異なることを伝える必要がある
引数を複数回使用するマクロに対して 副作用 のある式を渡した場合、予期せぬ結果が生じる可能性がある e.g.
code:rs
let mut x = 1;
let y = square!({
x += 1;
x
});
println!("x = {x}, y = {y}");
期待結果は x = 2, y = 4 だが、実際には x = 3, y = 6
解決策
1. 式を 1 度評価し、その結果をローカル変数に割り当てる
code:rs
macro_rules! square {
{ $e:expr } => {
{
let x = $e;
x * x
}
}
}
2. 構文要素指定子 expr を ident に置き換えて、式を受け付けないようにする
code:rs
macro_rules! square {
($e:ident) => { $e * $e };
}
これにより、println! や eprintln!、format! などと同じフォーマット構文を扱うことができる
また、フォーマット引数はコンパイル時にチェックされるため 型安全 である フォーマット時には Display や Debug トレイトを用いる
e.g.
code:rs
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