今回は、C++でたまに見かける「X-Macro(Xマクロ)」について解説していきます。
「enumに項目を足したのに、文字列の配列を直し忘れた…」
「同じリストを何箇所にも書いていてつらい」
「X-Macroって見たことあるけど、何をしてるの?」
こんな疑問はありませんか?
X-Macroは、定義リストを1箇所にまとめて、そこからenumや文字列テーブルを自動生成するテクニックです。うまく使えば「片方だけ直し忘れる」というバグが原理的に起きなくなります。
この記事を読み終えると、あなたはX-Macroの仕組み・書き方・メリットとデメリット・使うべき場面をしっかり理解できると思いますので、ぜひ最後まで読んでいただけると嬉しいです。
まず、何が問題なのか
X-Macroが解決する問題を先に見ましょう。敵の種類をenumで持ち、デバッグ用に名前も表示したいケースです。
// よくある書き方:同じリストが2箇所にある
enum class EnemyType {
Slime,
Goblin,
Dragon,
};
const char* kEnemyNames[] = {
"Slime",
"Goblin",
"Dragon",
};これ、動きはします。でも敵を1種類追加するたびに、2箇所を直す必要があります。しかも片方を忘れてもコンパイルは通ってしまうのが最悪で、実行時に名前がズレて表示されます。
enumについてはenumとenum classの違いの記事もどうぞ。
X-Macroの書き方
X-Macroでは、リストを1箇所だけに書きます。まずリスト本体を定義します。
// 定義リスト(ここだけを編集する)
#define ENEMY_LIST(X)
X(Slime, 100, 10)
X(Goblin, 200, 25)
X(Dragon, 999, 80)X は「あとで中身を差し替える枠」です。この X に別の定義を入れることで、同じリストから違うコードを生成できます。
#include <iostream>
// ① enumを生成する
#define X(name, hp, atk) name,
enum class EnemyType { ENEMY_LIST(X) Count };
#undef X
// ② 名前の配列を生成する
#define X(name, hp, atk) #name,
const char* kEnemyNames[] = { ENEMY_LIST(X) };
#undef X
// ③ ステータス表を生成する
struct EnemyStatus { int hp; int atk; };
#define X(name, hp, atk) { hp, atk },
const EnemyStatus kEnemyStatus[] = { ENEMY_LIST(X) };
#undef X展開されると、こうなります。
// ①はこう展開される
enum class EnemyType { Slime, Goblin, Dragon, Count };
// ②はこう展開される(#name で文字列になる)
const char* kEnemyNames[] = { "Slime", "Goblin", "Dragon" };使うときは普通の配列と同じです。
int main() {
EnemyType t = EnemyType::Goblin;
int i = static_cast<int>(t);
std::cout << kEnemyNames[i] << " HP:" << kEnemyStatus[i].hp << "n";
return 0;
}Goblin HP:200
ここが肝心です。敵を追加するときは ENEMY_LIST に1行足すだけ。enumも名前配列もステータス表も、全部まとめて更新されます。直し忘れが起こりようがありません。
メリット
- 二重管理がなくなる。追加も削除も1行で済む
- 更新漏れが原理的に起きない。ズレるという事故が消える
- 要素数も自動で合う(
Countを入れておけば個数も取れる)
デメリット
正直に言うと、これも小さくありません。
- 初見で読めない。知らない人にはまず意味が分からない
- IDEの補完・ジャンプが効きにくい。定義に飛べないことがある
- エラーメッセージが地獄。展開後のコードで怒られるので原因が追いにくい
- デバッガでステップ実行しづらい
マクロ全般の注意点は#defineとconstexprの違いの記事でも触れています。C++では基本的にマクロを避けるのが原則で、X-Macroはその例外として「割に合う場面がある」という位置づけです。
【重要】私が実際にX-Macroで助かった体験談
個人開発で、敵の種類をenumと名前配列で二重管理していたとき、デバッグ表示で敵の名前が1つずつズレるという現象に悩まされました。
原因は単純で、enumの途中に敵を1種類追加したのに、名前配列は末尾に足していたこと。インデックスが1つずれて、ゴブリンがドラゴンと表示されていたわけです。コンパイルは通るので、気づくまで時間がかかりました。
X-Macroに書き換えてからは、この種のバグが完全に消えました。1箇所しか編集する場所がないので、ズレようがありません。
ただし副作用もありました。マクロの中で書き間違えたとき、エラーが展開後の位置で出るので原因箇所がさっぱり分からない。数十行のエラーを前に固まったこともあります。便利さと引き換えに、詰まったときの調査は確実に重くなると実感しました。
X-Macro使用時のよくある失敗例と対処法
①#undef を忘れる
X という名前は汎用的すぎるので、#undef し忘れると別の場所と衝突します。使ったら必ず #undef X をセットで書きましょう。
②行末のバックスラッシュを間違える
複数行マクロは行末の で継続します。最終行に を付けてしまう、あるいは の後ろに空白が入ると壊れます。エラーが意味不明になる典型パターンです。
③何にでも使いたくなる
ハマると楽しくて多用しがちですが、読みにくさのコストは確実にあります。同じリストを3箇所以上で使うようなときに限定するのが、ちょうどいい塩梅です。
注意点
- 使ったら必ず
#undefする - チーム開発では要相談。読めない人には本当に読めない
- C++20以降なら、用途によってはconstexprやテンプレートで代替できることも
まとめ
- X-Macroは1つのリストから複数のコードを生成する手法
- enumと文字列テーブルの二重管理・更新漏れを根絶できる
- 代償は読みにくさ・エラーの追いにくさ
- 同じリストを何箇所も使うときに限定して使うのが吉
クセは強いですが、ハマる場面では本当に強力です。「enumと文字列の対応でバグった」経験がある人は、一度試してみてください。

