📚 背景知識(読んでから問題へ)
Day 053〜055 の FFI では「C の関数ポインタとの境界」を扱いました。今日から1か月(〜2026-10-25)はマクロです。最初の1問は、書く前に「macro_rules! が何をしていて、何をしていないか」を正確に掴むための予想問題にします。
macro_rules! は構文木(トークン木)を受け取り、構文木を返す関数です。C のプリプロセッサのような文字列置換ではありません。この違いから、次の3つの性質が出てきます。
| 性質 | C の #define | Rust の 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! もこれを使う |
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)、コンパイルエラー、のどれか。
答えを見る
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 の値を答えてください。
答えを見る
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行の出力を答えてください。
答えを見る
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 を答えてください。
答えを見る
next() が2か所に展開される。比較では 10、返すときは 20。比べて勝った値ではない値が返る。Q5: edition で結果が変わるか
macro_rules! which_rule {
($e:expr) => { "expr" };
(_) => { "underscore" };
}
println!("Q5 = {}", which_rule!(_));
edition = "2024" と edition = "2021" の両方を答えてください。
答えを見る
expr が _ にも一致するようになったため、1番目の規則で止まる。Q6: マクロが定義した関数は外から呼べるか
macro_rules! make_helper {
() => { fn helper() -> &'static str { "from macro" } };
}
make_helper!();
fn main() { println!("Q6 = {}", helper()); }
Q1 の衛生性と矛盾しないかも含めて答えてください。
答えを見る
$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か所だけ変えて通してください。
答えを見る
$(...),* は区切りの間にしかカンマを許さない。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)
u8u16u32u64i8i16i32i64の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文) |
|---|---|---|
| Q1 | 14 | マクロ内の x と呼び出し側の x は別の変数。2*2 + 10 |
| Q2a | 6 | expr の 1 + 2 は1つの塊 → (1+2)*2 |
| Q2b | 5 | tt はトークンのまま → 1 + 2 * 2 |
| Q3a | literal 0 | 最初の規則 (0) にトークン 0 が一致 |
| Q3b | some expr | expr として解析済みの塊は不透明で (0) と一致しない |
| Q3c | literal 0 | tt は解析せずトークンのまま渡す |
| Q4 | 20, calls = 2 | next() が2回展開される。比較は 10、返すのは 20 |
| Q5 | 2024: expr/2021: underscore | 2024 で expr が _ に一致するようになった |
| Q6 | コンパイルでき from macro | 項目は衛生的でない(混合衛生性) |
| Q7 | 定義時にコンパイルエラー | expr の後ろは => , ; だけ |
| Q8 | 6/エラー/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!(_) | underscore | expr | underscore |
k!(const { 1 })($e:expr の規則と (const $b:block) の規則がある) | const-block | expr | — |
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)が返るabはマクロ内で束縛したローカル変数なので衛生的。呼び出し側にaという変数があっても衝突しない(Q1 と同じ仕組み)- なぜ
letではなくmatchか:matchの検査対象に置いた一時値はmatch式全体の終わりまで生きるので、$aが一時値への参照を作る式でも寿命のエラーになりにくい。std のassert_eq!がこの形(rust-lang/rust 1.96.0 のlibrary/core/src/macros/mod.rs43〜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型に同じ impl | macro_rules!(impl_checksum!(u8, u16, ...)) | ブランケット実装 impl<T: 何か> Checksum for T にすると、外部の型に将来 Checksum を実装できなくなる(重複実装の禁止)。型ごとに独立した impl を作れるのはマクロだけ。std 自身も数値型の impl をマクロで量産している |
🪜 Step-by-Step 解説
コンパイラはソースをトークン列にし、括弧の対応を取ってトークン木にし、それを構文解析して AST にします。マッチャの $e:expr は「ここで式の構文解析を走らせ、成功した部分を1つの塊として受け取る」という意味です。
Q2a が 6 になるのは、展開後に括弧が付くからではなく、最初から「1 + 2 という式ノード」が * 2 の左オペランドになっているからです。
$e:expr で一度塊になったものを別のマクロに渡すと、渡された側から見ると1個の不透明なトークンです。Rust Reference は、ident lifetime tt 以外のフラグメントで捕まえたものは字句として照合できない、と説明しています。
実務ではマクロから別のマクロへ引数を転送するときに当たります。受け側がリテラルやキーワードで分岐しているなら、送り側は $t:tt(または $($t:tt)*)で受けます。(1 + 2) は1個の tt、1 + 2 は3個の tt です。
コンパイラは識別子に「どの展開で生まれたか」(構文コンテキスト)を付けて持っています。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() のように絶対パスで書くのはこのためです。
衛生性が守るのは「名前の衝突」だけです。$a を2回書けば next() は2か所に複製され、2回評価されます。C の #define MAX(a,b) ((a)>(b)?(a):(b)) と同じ落とし穴です。
対策は「1回だけ評価して名前を付ける」。assert_eq! assert_ne! dbg! はすべて match で束縛してから使っています。メタ変数を2回以上書いたら、副作用のある式が渡される前提でレビューする。
($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"); });
この制限はマクロの定義時点で検査されます(呼び出しが無くてもエラー)。
$($x:expr),* の , は区切りで、要素の間にだけ置けます。$(,)? を足すと「最後にカンマが0個か1個」を別に受けられます。
0 $(+ $x)* の先頭の 0 を消すと、sum!() の展開結果が空になって式として不正になります。0回の場合の展開結果を必ず確かめるのが定石です。
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)
読み方: 指しているのは呼び出しではなく定義の行です。note に置けるトークンがそのまま書いてあります。
エラー2: 末尾カンマ(Q8、$(,)? が無い版)
読み方: note の「$x:expr に一致させようとしていた」が、カンマの後ろで次の要素を待っていたことを示しています。
エラー3: 呼び出し側の変数をマクロの中から直接使おうとした(衛生性)
macro_rules! y_plus_one { () => { y + 1 }; }
fn main() { let y = 41; println!("{}", y_plus_one!()); }
help がはっきり「衛生性のせいで見えない」と言っています。直し方は ($y:ident) => { $y + 1 } として呼び出し側に名前を渡してもらうことです。
エラー4: 定義より前で使った(テキスト順のスコープ)
macro_rules! は関数と違い、ファイルの上から順に見えるようになります。別モジュールから使うには #[macro_use] か #[macro_export] が必要です。
エラー5: 繰り返しの深さの不一致と、参照で束縛した max
マッチャで $(...)* の中にあった $v は、トランスクライバでも $(...)* の中で使わなければなりません。
Q9 で assert_eq! の形(match (&$a, &$b))をそのまま真似た版です。マクロでも所有権の規則は展開後のコードにそのまま掛かります。
🌐 他言語との比較
| 観点 | C/C++ プリプロセッサ | C++ テンプレート | Go | TypeScript | Rust macro_rules! |
|---|---|---|---|---|---|
| 入力 | 文字列(トークン) | 型と定数 | (マクロ無し) | (マクロ無し) | トークン木をフラグメントとして解析 |
| 優先順位(Q2) | 壊れる。括弧を手で書く | 関数なので壊れない | — | — | 壊れない(expr は塊) |
| 名前の衝突(Q1) | 衝突する | 起きない | — | — | ローカル変数は起きない。項目は起きうる |
| 引数の複数回評価(Q4) | 起きる | 起きない | — | — | 起きる(match で束縛して避ける) |
| 可変個の引数 | __VA_ARGS__ | 可変長テンプレート | ...T | rest 引数 | $(...)* の繰り返し |
| コード生成の代替 | — | テンプレート | 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!など
#[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。記述は各資料の要旨で、原文の引用ではありません)
- The Rust Reference — Macros By Example(フラグメント指定子、繰り返し、衛生性、follow-set 制限): https://doc.rust-lang.org/reference/macros-by-example.html
- The Rust Edition Guide — Rust 2024: Macro Fragment Specifiers: https://doc.rust-lang.org/edition-guide/rust-2024/macro-fragment-specifiers.html
- The Little Book of Rust Macros: https://veykril.github.io/tlborm/
- rust-lang/rust 1.96.0 library/core/src/macros/mod.rs(assert_eq!。出題者が raw ファイルを取得して該当行を確認): https://github.com/rust-lang/rust/blob/1.96.0/library/core/src/macros/mod.rs
- rust-lang/rust 1.96.0 library/alloc/src/macros.rs(vec!。同上): https://github.com/rust-lang/rust/blob/1.96.0/library/alloc/src/macros.rs
- The Rust Programming Language — Macros: https://doc.rust-lang.org/book/ch20-05-macros.html
🚧 今日扱わなかった範囲(次回以降)
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!を書く