Day 056 — macro_rules! の展開結果を予想する:フラグメント・衛生性・評価回数・edition 2024

2026-09-26 🟣 上級者 / Phase 3 概念理解 マクロ①(宣言的マクロ macro_rules!)

📚 背景知識(読んでから問題へ)

Day 053〜055 の FFI では「C の関数ポインタとの境界」を扱いました。今日から1か月(〜2026-10-25)はマクロです。最初の1問は、書く前に「macro_rules! が何をしていて、何をしていないか」を正確に掴むための予想問題にします。

macro_rules! は構文木(トークン木)を受け取り、構文木を返す関数です。C のプリプロセッサのような文字列置換ではありません。この違いから、次の3つの性質が出てきます。

性質C の #defineRust の macro_rules!
何を受け取るか文字列(トークン列)。括弧の意味を知らないトークン木(() [] {} の対応が取れたまとまり)を、expr・ty・ident などのフラグメントとして構文解析して受け取る
引数の優先順位#define DBL(x) x * 2 に 1 + 2 → 1 + 2 * 2$e:expr で受けた 1 + 2 は1つの式のまま埋め込まれる → (1 + 2) * 2
名前の衝突(衛生性)マクロ内の変数名が呼び出し側の変数と衝突するマクロ内で let したローカル変数とラベルは呼び出し側の同名の変数と別物($crate も定義側で解決)。ただし関数・型などの項目(item)は衛生的でない

1つの規則は (マッチャ) => { トランスクライバ } の形で、上から順に試して最初に一致した規則だけが使われます。

書き方意味
$name:fragメタ変数。frag はフラグメント指定子(expr ident ty tt pat path block literal など。Rust Reference では 15 種)
$( ... ) sep rep繰り返し。rep は *(0回以上)+(1回以上)?(0か1回)、sep は区切り(省略可)
$(,)?「末尾のカンマがあってもなくてもよい」の定番。vec! も assert_eq! もこれを使う
edition 2024(Rust 1.85、2025-02 公開)で expr フラグメントの意味が変わりました。_ と const { ... } にも一致するようになり、以前の意味が欲しい場合は expr_2021 と書きます。同じマクロ定義が、Cargo.toml の edition だけで別の規則に一致することがあります(Q5 で実測)。

📝 問題

cargo new macro_day056(edition 2024)の src/main.rs に下のマクロ定義と呼び出しがあります。コンパイルも実行もせずに、Q1〜Q8 の結果を予想し、それぞれ理由を1〜2文で書いてください。答えは各問の「答えを見る」で開けます(予想を書いてから開いてください)。最後に記述問題 Q9・Q10 に答えてください。

Q1: 衛生性(ローカル変数)

macro_rules! square_plus_ten {
    ($e:expr) => {{
        let x = 10;
        $e * $e + x
    }};
}

let x = 2;
println!("Q1 = {}", square_plus_ten!(x));

14(2×2+10)、110(10×10+10)、コンパイルエラー、のどれか。

答えを見る
14 — マクロ内の let x = 10 は定義側の x、$e に入った x は呼び出し側の x(=2)。名前は同じでも別の変数なので 2*2 + 10。

Q2: expr と tt の違い

macro_rules! double_expr { ($e:expr) => { $e * 2 }; }
macro_rules! double_tt   { ($($t:tt)*) => { $($t)* * 2 }; }

println!("Q2a = {}", double_expr!(1 + 2));
println!("Q2b = {}", double_tt!(1 + 2));

Q2a と Q2b の値を答えてください。

答えを見る
Q2a = 6/Q2b = 5 — expr の 1 + 2 は1つの式の塊として * 2 の左に置かれる。tt はトークンのまま並べ直すだけなので C と同じ 1 + 2 * 2。

Q3: 捕まえた断片を別のマクロに渡す

macro_rules! what {
    (0)        => { "literal 0" };
    ($e:expr)  => { "some expr" };
}
macro_rules! forward_expr { ($e:expr) => { what!($e) }; }
macro_rules! forward_tt   { ($t:tt)   => { what!($t) }; }

println!("Q3a = {}", what!(0));
println!("Q3b = {}", forward_expr!(0));
println!("Q3c = {}", forward_tt!(0));

3行の出力を答えてください。

答えを見る
literal 0/some expr/literal 0 — forward_expr が 0 を expr として解析した後は不透明な式の塊になり、what! の (0)(トークン 0)とは一致しない。tt は解析せず渡すので一致する。

Q4: 引数は何回評価されるか

macro_rules! max_naive {
    ($a:expr, $b:expr) => { if $a > $b { $a } else { $b } };
}

let calls = Cell::new(0);
let next = || { calls.set(calls.get() + 1); calls.get() * 10 };   // 呼ぶたびに 10, 20, 30, ...
let m = max_naive!(next(), 5);
println!("Q4 = {m}, calls = {}", calls.get());

m と calls を答えてください。

答えを見る
Q4 = 20, calls = 2 — next() が2か所に展開される。比較では 10、返すときは 20。比べて勝った値ではない値が返る。

Q5: edition で結果が変わるか

macro_rules! which_rule {
    ($e:expr) => { "expr" };
    (_)       => { "underscore" };
}
println!("Q5 = {}", which_rule!(_));

edition = "2024" と edition = "2021" の両方を答えてください。

答えを見る
2024: expr/2021: underscore — edition 2024 で expr が _ にも一致するようになったため、1番目の規則で止まる。

Q6: マクロが定義した関数は外から呼べるか

macro_rules! make_helper {
    () => { fn helper() -> &'static str { "from macro" } };
}
make_helper!();

fn main() { println!("Q6 = {}", helper()); }

Q1 の衛生性と矛盾しないかも含めて答えてください。

答えを見る
コンパイルでき、Q6 = from macro — 衛生的なのはローカル変数・ラベル・$crate だけ。関数などの項目は呼び出し側のスコープに置かれる(混合衛生性)。

Q7: このマクロ定義はコンパイルできるか

macro_rules! when {
    ($cond:expr $body:block) => { if $cond $body };
}

when!(true { println!("hi"); }) のように使うつもりです。

答えを見る
定義の時点でコンパイルエラー — expr の直後に置けるのは => , ; だけ(follow-set 制限)。将来の構文追加で expr の終わりの解釈が変わらないようにするため。

Q8: 末尾カンマ

macro_rules! sum { ($($x:expr),*) => { 0 $(+ $x)* }; }

println!("{}", sum!(1, 2, 3));
println!("{}", sum!(1, 2, 3,));
println!("{}", sum!());

3行それぞれ、結果かコンパイルエラーかを答え、エラーになるものはマッチャを1か所だけ変えて通してください。

答えを見る
6/コンパイルエラー/0 — $(...),* は区切りの間にしかカンマを許さない。sum!() は0回の繰り返しで 0 だけ。マッチャを ($($x:expr),* $(,)?) にすると2行目も通る。

Q9(記述): max_naive! を直す

Q4 の問題を、マクロのまま直してください。std の assert_eq! が同じ問題をどう避けているかを参考にしてよいです。直した版が String のような Copy でない型でも使えるかどうかにも触れてください。

Q10(記述): マクロにすべきか、ジェネリック関数にすべきか

次の3つを、それぞれ macro_rules! とジェネリック関数のどちらで書くべきか、理由とともに答えてください。

  • (a) 2つの値の大きい方を返す max
  • (b) hashmap!{ "a" => 1, "b" => 2 } のように、個数が決まっていないキーと値の組から HashMap を作る
  • (c) u8 u16 u32 u64 i8 i16 i32 i64 の8つの型に、同じ中身の impl Checksum for T { ... } を書く

💡 ヒント

ヒント1 — 方向性

「文字列を貼り付ける」と考えると Q2・Q3・Q4 を外します。$e:expr で捕まえた瞬間に構文解析が終わり、1つの式の塊になると考えてください。塊は優先順位を壊さず(Q2)、中身を覗かれず(Q3)、しかし書いた回数だけ複製される(Q4)。

ヒント2 — アプローチ
  • 衛生性は「どこで書かれた名前か」で解決先が決まります。マクロ定義の中で書いた x は定義側、$e として渡された x は呼び出し側の文脈です(Q1)
  • macro_rules! の衛生性は「混合(mixed site)」。ローカル変数・ラベル・$crate は定義側、それ以外(関数・型・モジュールなどの項目)は呼び出し側で解決されます(Q6)
  • Rust Reference の "Follow-set Ambiguity Restrictions" に、expr の直後に置けるトークンが決まっています(Q7)
  • Q8 の3行目は * が「0回以上」であることを思い出してください
ヒント3 — コード骨格

Q9 は「一度だけ評価して、名前を付けてから使う」です。

macro_rules! max_once {
    ($a:expr, $b:expr) => {
        match ($a, $b) {
            (a, b) => { /* a と b を使う。$a と $b はもう書かない */ }
        }
    };
}

let a = $a; ではなく match を使う理由は、assert_eq! の実装(match (&$left, &$right) { (left_val, right_val) => ... })を見て考えてください。

✅ 模範解答

答え(一覧)

問答え理由(1文)
Q114マクロ内の x と呼び出し側の x は別の変数。2*2 + 10
Q2a6expr の 1 + 2 は1つの塊 → (1+2)*2
Q2b5tt はトークンのまま → 1 + 2 * 2
Q3aliteral 0最初の規則 (0) にトークン 0 が一致
Q3bsome exprexpr として解析済みの塊は不透明で (0) と一致しない
Q3cliteral 0tt は解析せずトークンのまま渡す
Q420, calls = 2next() が2回展開される。比較は 10、返すのは 20
Q52024: expr/2021: underscore2024 で expr が _ に一致するようになった
Q6コンパイルでき from macro項目は衛生的でない(混合衛生性)
Q7定義時にコンパイルエラーexpr の後ろは => , ; だけ
Q86/エラー/0$(,)? を足すと2行目も通る

検証用コード(cargo test / cargo run でそのまま確認できる完全版)

cargo new macro_day056 の src/main.rs をこれで置き換えます。外部クレートは不要、Cargo.toml は edition = "2024" のままです。

use std::cell::Cell;

// Q1: 衛生性(ローカル変数)
macro_rules! square_plus_ten {
    ($e:expr) => {{
        let x = 10;
        $e * $e + x
    }};
}

// Q2: expr は1つの式として埋め込まれる / tt はトークン列のまま
macro_rules! double_expr {
    ($e:expr) => {
        $e * 2
    };
}
macro_rules! double_tt {
    ($($t:tt)*) => {
        $($t)* * 2
    };
}

// Q3: 一度 expr として捕まえた断片は、転送先で字句(リテラル)とは一致しない
macro_rules! what {
    (0) => {
        "literal 0"
    };
    ($e:expr) => {
        "some expr"
    };
}
macro_rules! forward_expr {
    ($e:expr) => {
        what!($e)
    };
}
macro_rules! forward_tt {
    ($t:tt) => {
        what!($t)
    };
}

// Q4: 引数の評価回数
macro_rules! max_naive {
    ($a:expr, $b:expr) => {
        if $a > $b { $a } else { $b }
    };
}
macro_rules! max_once {
    ($a:expr, $b:expr) => {
        match ($a, $b) {
            (a, b) => {
                if a > b { a } else { b }
            }
        }
    };
}

// Q5: edition 2024 で expr が `_` と `const { }` に一致するようになった
macro_rules! which_rule {
    ($e:expr) => {
        "expr"
    };
    (_) => {
        "underscore"
    };
}

// Q6: 項目(fn)は衛生的でない
macro_rules! make_helper {
    () => {
        fn helper() -> &'static str {
            "from macro"
        }
    };
}
make_helper!();

// Q8: 繰り返しと末尾カンマ
macro_rules! sum {
    ($($x:expr),* $(,)?) => {
        0 $(+ $x)*
    };
}

fn main() {
    let x = 2;
    println!("Q1 = {}", square_plus_ten!(x));

    println!("Q2a = {}", double_expr!(1 + 2));
    println!("Q2b = {}", double_tt!(1 + 2));

    println!("Q3a = {}", what!(0));
    println!("Q3b = {}", forward_expr!(0));
    println!("Q3c = {}", forward_tt!(0));

    let calls = Cell::new(0);
    let next = || {
        calls.set(calls.get() + 1);
        calls.get() * 10
    };
    let m = max_naive!(next(), 5);
    println!("Q4a = {m}, calls = {}", calls.get());
    calls.set(0);
    let m = max_once!(next(), 5);
    println!("Q4b = {m}, calls = {}", calls.get());

    println!("Q5 = {}", which_rule!(_));

    println!("Q6 = {}", helper());

    println!("Q8a = {}", sum!(1, 2, 3,));
    println!("Q8b = {}", sum!());
}

#[cfg(test)]
mod tests {
    use std::cell::Cell;

    #[test]
    fn q1_local_variables_are_hygienic() {
        let x = 2;
        assert_eq!(square_plus_ten!(x), 14); // 2*2 + 10。マクロ内の x=10 は呼び出し側の x を隠さない
    }

    #[test]
    fn q2_expr_is_one_node_tt_is_raw_tokens() {
        assert_eq!(double_expr!(1 + 2), 6); // (1 + 2) * 2
        assert_eq!(double_tt!(1 + 2), 5); //   1 + 2 * 2
    }

    #[test]
    fn q3_forwarded_expr_is_opaque() {
        assert_eq!(what!(0), "literal 0");
        assert_eq!(forward_expr!(0), "some expr");
        assert_eq!(forward_tt!(0), "literal 0");
    }

    #[test]
    fn q4_naive_max_evaluates_twice() {
        let calls = Cell::new(0);
        let next = || {
            calls.set(calls.get() + 1);
            calls.get() * 10
        };
        assert_eq!(max_naive!(next(), 5), 20);
        assert_eq!(calls.get(), 2);
        calls.set(0);
        assert_eq!(max_once!(next(), 5), 10);
        assert_eq!(calls.get(), 1);
        // 値で束縛するので Copy でない型も渡せる
        assert_eq!(max_once!(String::from("b"), String::from("a")), "b");
    }

    #[test]
    fn q5_edition_2024_expr_matches_underscore() {
        // Cargo.toml の edition を 2021 にすると "underscore" になる
        assert_eq!(which_rule!(_), "expr");
    }

    #[test]
    fn q6_items_are_not_hygienic() {
        assert_eq!(super::helper(), "from macro");
    }

    #[test]
    fn q8_trailing_comma_and_empty() {
        assert_eq!(sum!(1, 2, 3,), 6);
        assert_eq!(sum!(1, 2, 3), 6);
        assert_eq!(sum!(), 0);
    }
}

実測(出題者の環境: rustc 1.96.0 / edition 2024 / Apple M1 / macOS 26.5.1、2026-09-26)

cargo run -q の出力(実測):

Q1 = 14
Q2a = 6
Q2b = 5
Q3a = literal 0
Q3b = some expr
Q3c = literal 0
Q4a = 20, calls = 2
Q4b = 10, calls = 1
Q5 = expr
Q6 = from macro
Q8a = 6
Q8b = 0

cargo test -q → test result: ok. 7 passed; 0 failed(実測)。cargo clippy -q --all-targets の警告は 0 件(実測)。

edition を変えた比較(実測。rustc --edition 2021|2024 で単体ファイルをビルド):

入力$e:expr(2021)$e:expr(2024)$e:expr_2021(2024)
which_rule!(_)underscoreexprunderscore
k!(const { 1 })($e:expr の規則と (const $b:block) の規則がある)const-blockexpr—

rustc 1.85(edition 2024 が安定化した版)でも 2024 の列と同じ結果でした(実測。rustc +1.85)。

Q9 の回答

macro_rules! max_once {
    ($a:expr, $b:expr) => {
        match ($a, $b) {
            (a, b) => { if a > b { a } else { b } }
        }
    };
}
  • $a $b をトランスクライバに1回だけ書き、match のパターンで名前を付けてからその名前を使う。next() の呼び出しは1回になり(実測 calls = 1)、比較した値そのもの(10)が返る
  • a b はマクロ内で束縛したローカル変数なので衛生的。呼び出し側に a という変数があっても衝突しない(Q1 と同じ仕組み)
  • なぜ let ではなく match か: match の検査対象に置いた一時値は match 式全体の終わりまで生きるので、$a が一時値への参照を作る式でも寿命のエラーになりにくい。std の assert_eq! がこの形(rust-lang/rust 1.96.0 の library/core/src/macros/mod.rs 43〜44 行目: match (&$left, &$right) { (left_val, right_val) => ...)
  • assert_eq! は比較するだけで値を消費してはいけないので参照で受ける。max は値を返すのが目的なので値で受ける。assert_eq! の真似で match (&$a, &$b) にして *a を返すと、String で E0507(実測。エラー5)。値で受けた版は max_once!(String::from("b"), String::from("a")) が "b" を返す(テストで確認)

Q10 の回答

選ぶもの理由
(a) maxジェネリック関数 fn max<T: Ord>(a: T, b: T) -> T(std に std::cmp::max)関数なら引数は必ず1回だけ評価され(Q4 が構造的に起きない)、T: Ord が型で書け、エラーも関数の境界で出る。マクロにする理由が無い
(b) hashmap!{ k => v, ... }macro_rules!関数は引数の個数を固定する(可変長引数が無い)。=> のような独自の区切りも関数では書けない。ただし std に HashMap::from([("a", 1), ("b", 2)]) があるので、マクロの利点は見た目と with_capacity 程度。規約で HashMap::from を選ぶのも正解
(c) 8型に同じ implmacro_rules!(impl_checksum!(u8, u16, ...))ブランケット実装 impl<T: 何か> Checksum for T にすると、外部の型に将来 Checksum を実装できなくなる(重複実装の禁止)。型ごとに独立した impl を作れるのはマクロだけ。std 自身も数値型の impl をマクロで量産している
判断の軸: 「型で表せることは関数・トレイトで、構文(個数・区切り・項目の生成)はマクロで」

🪜 Step-by-Step 解説

1
マクロは「構文解析の途中」で動く

コンパイラはソースをトークン列にし、括弧の対応を取ってトークン木にし、それを構文解析して AST にします。マッチャの $e:expr は「ここで式の構文解析を走らせ、成功した部分を1つの塊として受け取る」という意味です。

Q2a が 6 になるのは、展開後に括弧が付くからではなく、最初から「1 + 2 という式ノード」が * 2 の左オペランドになっているからです。

2
塊は中を覗かれない — Q3 の「不透明性」

$e:expr で一度塊になったものを別のマクロに渡すと、渡された側から見ると1個の不透明なトークンです。Rust Reference は、ident lifetime tt 以外のフラグメントで捕まえたものは字句として照合できない、と説明しています。

実務ではマクロから別のマクロへ引数を転送するときに当たります。受け側がリテラルやキーワードで分岐しているなら、送り側は $t:tt(または $($t:tt)*)で受けます。(1 + 2) は1個の tt、1 + 2 は3個の tt です。

3
衛生性は「名前がどこで書かれたか」 — Q1 と Q6

コンパイラは識別子に「どの展開で生まれたか」(構文コンテキスト)を付けて持っています。Q1 の展開結果を、見えない印を付けて書くとこうなります。

{ let x#macro = 10; x#caller * x#caller + x#macro }   // x#caller = 2 → 14

Q6 の helper は項目です。ローカル変数・ラベル・$crate だけが定義側で解決され、関数・型・モジュールなどは呼び出し側で解決されます。

裏返すと、#[macro_export] したマクロの中で HashMap と書くと、利用者が use していないとエラーになります。std のマクロが $crate::vec::Vec::new() のように絶対パスで書くのはこのためです。

4
評価回数は衛生性とは無関係 — Q4

衛生性が守るのは「名前の衝突」だけです。$a を2回書けば next() は2か所に複製され、2回評価されます。C の #define MAX(a,b) ((a)>(b)?(a):(b)) と同じ落とし穴です。

対策は「1回だけ評価して名前を付ける」。assert_eq! assert_ne! dbg! はすべて match で束縛してから使っています。メタ変数を2回以上書いたら、副作用のある式が渡される前提でレビューする。

5
follow-set 制限は「将来の構文」のため — Q7

($cond:expr $body:block) が禁止されるのは、将来 Rust の式の構文が増えたときに expr の終わりの解釈が変わってマクロが壊れるのを防ぐためです。expr と stmt の後ろには => , ;、pat なら => , = | if in しか置けません。

macro_rules! when {
    ($cond:expr, $body:block) => { if $cond $body };
}
// when!(true, { println!("hi"); });

この制限はマクロの定義時点で検査されます(呼び出しが無くてもエラー)。

6
繰り返しは区切りと回数を別々に指定する — Q8

$($x:expr),* の , は区切りで、要素の間にだけ置けます。$(,)? を足すと「最後にカンマが0個か1個」を別に受けられます。

0 $(+ $x)* の先頭の 0 を消すと、sum!() の展開結果が空になって式として不正になります。0回の場合の展開結果を必ず確かめるのが定石です。

7
edition 2024 の expr — Q5

Rust 本体の式の構文が増えた(_ は分割代入の左辺、const {} はインライン const)のに、マクロの expr が追随していなかったのを揃えた変更です。既存のマクロが黙って別の規則に一致するようになるので edition で分けました。

cargo fix --edition は影響しうる $e:expr を $e:expr_2021 に書き換えます(Edition Guide の説明。今日は手で書き換えて比較)。マクロはマクロを定義したクレートの edition で解釈されます。

🧭 設計思想・なぜこう書くのか

Rust のマクロが「構文木を扱う」ことで得たものと払ったもの

得たもの払ったもの(今日の問題)
展開結果の構文木の形が保証される(Q2)捕まえた断片が不透明になり、転送時に tt を使い分ける必要がある(Q3)
ローカル変数の名前衝突が起きない(Q1)項目は衛生的でないという非対称を覚える必要がある(Q6)
将来の構文追加でマクロが壊れない(Q7)expr の後ろに自由にトークンを置けない(Q7)
edition ごとに言語を進化させられる(Q5)同じマクロの意味が edition で変わる(Q5)

判断の記録: 今日の問題を「予想問題」にした理由

判断内容
採った案8つの「結果を予想する」問題+2つの記述。すべて出題者の環境で実測した出力を答えにした
迷った案1: いきなり hashmap! を実装させる実装は明日以降。マクロの実装で詰まる原因の大半は Q3(転送で一致しない)と Q7(follow-set)で、これを知らずに書くとエラーから原因にたどり着けない
迷った案2: cargo expand で展開結果を見せる出題者の環境に nightly も cargo-expand も無く、確かめていない展開結果を載せないために見送った。代わりに「実行結果」と「呼び出し回数」で展開の形を間接的に確かめた
迷った案3: proc-macro と並べるテーマ表で #13 に分かれているので今日は扱わない
前提が変わったらmacros 2.0(macro キーワード、定義側の衛生性)が安定化したら Q6 の答えが変わる。2026-09-26 時点では nightly のみ

🚨 コンパイルエラーが出た場合

以下はすべて出題者の環境(rustc 1.96.0、edition 2024、aarch64 macOS)で rustc --edition 2024 を実行して実際に出た出力です。

エラー1: follow-set 制限(Q7)

error: `$cond:expr` is followed by `$body:block`, which is not allowed for `expr` fragments --> e1.rs:2:17 | 2 | ($cond:expr $body:block) => { | ^^^^^^^^^^^ not allowed after `expr` fragments | = note: allowed there are: `=>`, `,` or `;`

読み方: 指しているのは呼び出しではなく定義の行です。note に置けるトークンがそのまま書いてあります。

エラー2: 末尾カンマ(Q8、$(,)? が無い版)

error: unexpected end of macro invocation --> e2.rs:4:41 | 1 | macro_rules! sum { | ---------------- when calling this macro ... 4 | fn main() { println!("{}", sum!(1, 2, 3,)); } | ^ missing tokens in macro arguments | note: while trying to match meta-variable `$x:expr` --> e2.rs:2:8 | 2 | ($($x:expr),*) => { 0 $(+ $x)* }; | ^^^^^^^

読み方: note の「$x:expr に一致させようとしていた」が、カンマの後ろで次の要素を待っていたことを示しています。

エラー3: 呼び出し側の変数をマクロの中から直接使おうとした(衛生性)

macro_rules! y_plus_one { () => { y + 1 }; }
fn main() { let y = 41; println!("{}", y_plus_one!()); }
error[E0425]: cannot find value `y` in this scope --> e3.rs:2:13 | 2 | () => { y + 1 }; | ^ not found in this scope ... 6 | println!("{}", y_plus_one!()); | ------------- in this macro invocation | help: an identifier with the same name exists, but is not accessible due to macro hygiene --> e3.rs:5:9 | 5 | let y = 41; | ^

help がはっきり「衛生性のせいで見えない」と言っています。直し方は ($y:ident) => { $y + 1 } として呼び出し側に名前を渡してもらうことです。

エラー4: 定義より前で使った(テキスト順のスコープ)

error: cannot find macro `late` in this scope --> e4.rs:1:28 | 1 | fn main() { println!("{}", late!()); } | ^^^^ consider moving the definition of `late` before this call | note: a macro with the same name exists, but it appears later

macro_rules! は関数と違い、ファイルの上から順に見えるようになります。別モジュールから使うには #[macro_use] か #[macro_export] が必要です。

エラー5: 繰り返しの深さの不一致と、参照で束縛した max

error: variable `v` is still repeating at this depth --> e5.rs:6:17 | 6 | ($name, $v) | ^^

マッチャで $(...)* の中にあった $v は、トランスクライバでも $(...)* の中で使わなければなりません。

error[E0507]: cannot move out of `*a` which is behind a shared reference --> e6.rs:4:38 | 4 | (a, b) => { if *a > *b { *a } else { *b } } | ^^ move occurs because `*a` has type `String`, which does not implement the `Copy` trait

Q9 で assert_eq! の形(match (&$a, &$b))をそのまま真似た版です。マクロでも所有権の規則は展開後のコードにそのまま掛かります。

🌐 他言語との比較

観点C/C++ プリプロセッサC++ テンプレートGoTypeScriptRust macro_rules!
入力文字列(トークン)型と定数(マクロ無し)(マクロ無し)トークン木をフラグメントとして解析
優先順位(Q2)壊れる。括弧を手で書く関数なので壊れない——壊れない(expr は塊)
名前の衝突(Q1)衝突する起きない——ローカル変数は起きない。項目は起きうる
引数の複数回評価(Q4)起きる起きない——起きる(match で束縛して避ける)
可変個の引数__VA_ARGS__可変長テンプレート...Trest 引数$(...)* の繰り返し
コード生成の代替—テンプレートgo generate—build.rs、proc-macro(#13)

Go は言語にマクロを入れず、go generate でソースファイルを生成してコミットする方針を取りました。生成物がレビューできる代わりに、生成し忘れや差分のノイズが出ます。Rust の macro_rules! は生成物をファイルに残さない代わりに、cargo expand のような道具を使わないと展開後の形が見えません。

🏭 実務での使いどころ

  • std の定番: vec!($(,)?)、assert_eq!(match で1回評価)、println!(書式はコンパイラ組み込みの format_args! が解析)、matches!
  • 数値型への一括 impl(Q10 (c)): std 自身も各整数型のメソッドや From 実装をマクロで量産している
  • テストの表駆動: test_cases! { add_small: (1, 2) => 3 } から #[test] fn add_small() を生成する。項目が衛生的でない(Q6)ことを逆に利用する
  • ログ・計測のラッパ: 呼び出し位置(file!() line!())を埋め込みたいときは関数ではなくマクロ。tracing::info! など
レビューで見る3点: メタ変数を2回以上書いていないか(Q4)/#[macro_export] するマクロのパスが $crate:: か絶対パスか(Step 3)/0回の繰り返しと末尾カンマで展開が壊れないか(Q8)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
「Rust のマクロは衛生的だから C の落とし穴は全部無い」衛生性を「安全」全般と取り違える衛生性が防ぐのはローカル変数とラベルの名前衝突だけ。複数回評価(Q4)は防がない。項目は衛生的でない(Q6)
$e:expr を受けて別のマクロの (0) で分岐させる文字列置換のつもりで考える捕まえた expr は不透明(Q3)。tt で受けて転送する
末尾カンマのために $($x:expr,)* と書く区切りと終端を同じものと考えるそれだと最後のカンマが必須になる。$($x:expr),* $(,)? が定番
#[macro_export] したマクロの中で HashMap を裸で書く定義しているクレートでは use してあるので動く型名は呼び出し側で解決される。::std::collections::HashMap か $crate::... で書く
edition を上げてもマクロの意味は変わらないedition は構文の話でマクロは関係ないと思う2024 で expr が _ と const {} に一致するようになった(Q5)
何でもマクロにすれば短く書けるボイラープレートが減るのが見える関数で書けるものは関数に(Q10 (a))。マクロはエラーが読みにくく IDE の補完も弱い

📎 一次資料(参照日: 2026-09-26。記述は各資料の要旨で、原文の引用ではありません)

🚧 今日扱わなかった範囲(次回以降)

Rust Reference のフラグメント指定子は 15 種あり、今日の問題で実際に使ったのは expr expr_2021 tt block ident の 5 種(3分の1)です。

扱わなかったもの理由・予定
ty pat path vis item meta lifetime literal stmt pat_param明日以降の実装問題(hashmap!・impl_checksum!)で ty ident vis を中心に使う
tt マンチャ、内部規則(@inner)、recursion_limit(既定 128)マクロ① の後半で DSL を1つ書く問題として扱う
$crate の実測2つのクレートが必要。#[macro_export] を扱う回にまとめる
手続き的マクロPhase 3 #13 マクロ②(2026-10-25 以降)
展開結果を直接見る(cargo expand)出題者の環境に nightly が無く未検証
コンパイル時間への影響今日は測っていない。cargo build --timings で測れる

➡️ 次のステップ

  • 発展: Q3 の what! に ($l:literal) => { what!($l) } 経由で 0 を渡すとどうなるか、予想してから試す
  • 次回予告: マクロ① の2問目(実装)— hashmap!{ k => v, ... } を $(,)? 対応・with_capacity 付きで実装し、要素数をコンパイル時に数える count! を書く

🎯 自己評価(解いた後に記入)

予想が当たった数: 0 / 11 (当たった問にチェック)

自分の回答(Q1〜Q8 の予想と理由、Q9・Q10)

気づき・メモ