メタプログラミングは、プログラムを「データとして読み書きするプログラム」を書く技法です。同じ形のメソッドを手で並べる代わりにループで生成したり、クラスが作られる瞬間に検査を差し込んだり、コンパイラに計算をさせたりできます。この記事では、仕組みを実行時とコンパイル時の2系統に分けたうえで、Ruby・Python・JavaScript・C++・Rustの代表的な書き方を、手元で実行して結果を確かめたコードで示します。便利さの裏で読みにくさと遅さを抱える技法なので、使うべきでない場面も具体的に書きます。
まとめ:メタプログラミングの要点
- 定義:コード(クラス・メソッド・構文木・型)を値として扱い、生成・変更・検査するプログラミング技法
- 2系統:実行時に動くもの(Rubyのdefine_method、Pythonのデコレータとメタクラス、JavaScriptのProxy)と、コンパイル時に動くもの(C++のテンプレートとconstexpr、Rustのマクロ)がある
- リフレクションは「実行時に型やメソッドを調べる・呼ぶ」仕組みで、実行時メタプログラミングの部品の一つ
- Rubyで
method_missingを使うならrespond_to_missing?を必ず対で書く。書かないとrespond_to?が嘘をつく - 実測ではRubyの
method_missing経由の呼び出しは通常のdefより約5倍遅かった(500万回で3.24秒対0.64秒) - 採用を検討するのは「外部の定義に合わせて同じ形のコードを生成する」「フレームワークとして利用者のコードを受け取る」とき。業務ロジックの数箇所を短くするためには使わない
メタプログラミングの意味:プログラムを扱うプログラム
日本語版Wikipediaは、メタプログラミングを「プログラムを記述するプログラム」を書くこと、またはそのプログラムと説明しています(Wikipedia「メタプログラミング」)。普通のプログラムが扱うデータは数値や文字列ですが、メタプログラムが扱うデータはクラス・メソッド・構文木・型といった「プログラムそのもの」です。
身近な例はRuby on Railsです。has_many :postsと1行書くと、postsやposts=などのメソッドがクラスに生えます。利用者が書いていないメソッドを、フレームワークのコードが実行時に定義しているわけです。Pythonの@dataclassも同じで、フィールド宣言から__init__や__eq__を生成します。
メタ言語・メタプログラム・リフレクションとの違い
似た言葉が多いので、対象と動く時点で区別します。
| 用語 | 指すもの | 例 |
|---|---|---|
| メタ言語 | 言語を記述するための言語 | BNF、型理論の記法 |
| メタプログラム | プログラムを生成・変更するプログラム | Railsのhas_many、コード生成ツール |
| メタプログラミング | メタプログラムを書く技法 | define_method、マクロ、テンプレート |
| リフレクション | 実行時に型やメソッドを調べ・呼ぶ機能 | JavaのClass.forName、Rubyのsend |
「メタ言語」は文法や意味を記述する道具の話で、コードを生成する技法とは別物です。一方のリフレクションは、メタプログラミングのうち「実行時に調べて呼ぶ」部分を担う部品です。言語ごとのリフレクションAPIの違いはリフレクションの仕組みを言語横断で解説した記事にまとめています。
実行時とコンパイル時の2系統
メタプログラミングは、コードを操作するタイミングで2つに分かれます。実行時に動くものは柔軟な反面、動的に生成した定義を型チェッカやIDEがそのままでは把握しにくい場合があります。型スタブや、Pythonのdataclass_transformなどで解析に必要な情報を補える仕組みもあります。コンパイル時に動くものは生成結果がコンパイラに検査されます。コード生成や定数評価そのものを実行前に済ませられますが、生成されたコードの実行コストは残り、書ける操作にも制約があります。
| 言語 | 主な仕組み | 動く時点 | 導入 |
|---|---|---|---|
| Ruby | define_method・method_missing・Refinements | 実行時 | Refinements 2.0(実験的) |
| Python | デコレータ・メタクラス・__init_subclass__ | 実行時(クラス定義時) | デコレータ2.4/__init_subclass__ 3.6 |
| JavaScript | Proxy・Reflect | 実行時 | ES2015 |
| C++ | テンプレート・constexpr・consteval | コンパイル時 | constexpr C++11/consteval C++20 |
| Rust | macro_rules!・手続き型マクロ | コンパイル時 | 独自derive 1.15/属性・関数風の手続き型マクロ 1.30 |
Lispのマクロによる構文の変換や、Rustのマクロによるトークン列の変換は、コンパイル時メタプログラミングの代表例です。Rustの手続き型マクロの入出力は構文木そのものではなく、トークン列です。どちらの系統を選ぶかは言語でほぼ決まるため、以下は言語ごとに見ていきます。
Rubyによるメソッドの動的定義
Rubyではクラス定義そのものが実行されるコードであり、メソッドの動的定義や未定義メソッドへのフックなどを利用できます。以下はRuby 4.0.6で実行した結果です。
define_methodによる同形メソッドの生成
define_methodは、名前とブロックからインスタンスメソッドを定義します(Module#define_method)。設定項目ごとにゲッターとセッターを並べる代わりに、配列から生成できます。
class Config
%w[host port user].each do |key|
define_method(key) { @values[key] }
define_method("#{key}=") { |v| @values[key] = v }
end
def initialize; @values = {}; end
end
c = Config.new
c.port = 5432
p c.port # => 5432
p Config.instance_methods(false).sort # => [:host, :host=, :port, :port=, :user, :user=]
項目が3つなら手で書いた方が読みやすいのも事実です。生成する価値が出るのは、項目の一覧が設定ファイルやDBのカラム定義など外部から来るときです。
method_missingとrespond_to_missing?の併用
method_missingは、存在しないメソッドが呼ばれたときに呼ばれるフックです。Active Recordのfind_by_emailのような動的ファインダーも、method_missingとrespond_to_missing?の組で実装されています(rails/activerecord dynamic_matchers.rb)。method_missingだけを定義すると、respond_to?がfalseを返し、methodでメソッドオブジェクトも取り出せません。respond_to?が問い合わせる先がrespond_to_missing?だからです(Object#respond_to_missing?)。
class Ghost
def method_missing(name, *args)
name.to_s.start_with?("find_by_") ? "#{name.to_s.delete_prefix('find_by_')}=#{args.first}" : super
end
end
class Ghost2 < Ghost
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("find_by_") || super
end
end
p Ghost.new.find_by_email("[email protected]") # => "[email protected]"
p Ghost.new.respond_to?(:find_by_email) # => false(嘘をつく)
p Ghost2.new.respond_to?(:find_by_email) # => true
begin
Ghost.new.method(:find_by_email)
rescue NameError => e
p e.class # => NameError
end
p Ghost2.new.method(:find_by_email).call("x") # => "email=x"
公式ドキュメントは、処理しないと決めた名前ではsuperを呼んで祖先クラスに任せるよう書いています(BasicObject#method_missing)。superを呼ばず、未対応の名前で何も処理しない実装にすると、タイプミスでもNoMethodErrorが発生せず、nilを返してしまうことがあります。
Refinementsによるクラス拡張の影響範囲の限定
Rubyでは既存クラス(Stringなど)を後から開いてメソッドを足せます。これがモンキーパッチで、プロセス全体に効くため、別のgemが同名のメソッドを足すと衝突します。Refinementsは、変更をusingしたファイル(またはクラス・モジュールの定義)の中だけに閉じ込める仕組みです(Refinements)。Ruby 2.0.0で実験的機能として入り(Ruby 2.0.0-p0リリース告知)、2.1でトップレベルのusingの実験扱いが外れました。
module Shout
refine String do
def shout = upcase + "!"
end
end
p "hi".respond_to?(:shout) # => false(using前は見えない)
using Shout
p "hi".shout # => "HI!"
標準クラスに便利メソッドを足したくなったら、まずRefinementsで書けないかを検討します。この記事のコードはRuby 4.0.6で確かめましたが、Refinementsを含む構文の版ごとの変更点はRubyの最新バージョンと変更点の記事で追っています。
Pythonのクラス生成と関数装飾
Pythonのメタプログラミングは、関数を包むデコレータ、サブクラス作成時に呼ばれる__init_subclass__、クラスそのものを作るメタクラスの3段階です。上ほど軽く、下ほど強力で副作用も大きくなります。以下はPython 3.14.6で実行しました。
デコレータによる関数のラップ
関数デコレータは、定義された関数を受け取り、その名前に束縛するオブジェクトを返す呼び出し可能オブジェクトです。次の例では、元の関数を包む別の関数を返します。@timedと書くのはwork = timed(work)の糖衣構文で、Python 2.4で導入されました(PEP 318)。
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__}: {time.perf_counter() - start:.4f}s")
return wrapper
@timed
def work(n):
return sum(range(n))
print(work(1_000_000), work.__name__)
# work: 0.0444s
# 499999500000 work
functools.wrapsを付けないとwork.__name__がwrapperに変わるため、関数名を使うログやイントロスペクションに影響します。元の関数をラップする場合は、通常functools.wrapsで名前やドキュメント文字列を引き継ぎます。ただし、スタックトレースのフレーム名は変更されません。
__init_subclass__によるサブクラスの自動登録
プラグインの登録のように「サブクラスが定義されたら何かしたい」用途は、Python 3.6で入った__init_subclass__で書けます(PEP 487)。以前はメタクラスが必要だった処理です。
class Plugin:
registry = {}
def __init_subclass__(cls, /, name=None, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[name or cls.__name__.lower()] = cls
class CsvExporter(Plugin, name="csv"): pass
class JsonExporter(Plugin): pass
print(Plugin.registry)
# {'csv': <class '__main__.CsvExporter'>, 'jsonexporter': <class '__main__.JsonExporter'>}
クラスメソッドのclsと同じく、ここでもクラスが値として渡されています。clsの扱いはPythonの@classmethodとstaticmethodの違いで詳しく説明しています。
メタクラスが必要になる場面
メタクラスは「クラスを作るクラス」で、クラス本体の名前空間を受け取って検査や書き換えができます。次の例は、大文字を含む属性名を持つクラスの定義そのものを拒否します。
class Upper(type):
def __new__(mcls, name, bases, ns):
if any(k != k.lower() for k in ns if not k.startswith("__")):
raise TypeError(f"{name}: 属性名は小文字のみ")
return super().__new__(mcls, name, bases, ns)
class Ok(metaclass=Upper):
value = 1
class Ng(metaclass=Upper): # TypeError: Ng: 属性名は小文字のみ
Value = 1
クラスのメタクラスには、明示したメタクラスと各基底クラスのメタクラスすべてのサブタイプになる候補が選ばれます。その候補が存在しない場合にmetaclass conflictになります。登録や検証だけなら__init_subclass__かクラスデコレータで足りるため、メタクラスはORMのようにクラスの作り方そのものを変えるときに限るのが無難です。@dataclassも、フィールドの宣言から__init__などのメソッドを生成してクラスに追加するデコレータです(PEP 557、Python 3.7)。
JavaScriptのProxyとReflectによるプロパティ操作の捕捉
JavaScriptではES2015でProxyとReflectが入り、プロパティの読み書き・削除・列挙などを横取りできるようになりました(MDN: Proxy)。次の例は、タイプミスしたプロパティ名をundefinedで流さず例外にし、書き込みをログに残します。Node.js v26.5.0で実行しました。
const target = { name: "issoh" };
const audited = new Proxy(target, {
get(t, prop, receiver) {
if (!(prop in t)) throw new TypeError(`未定義のプロパティ: ${String(prop)}`);
return Reflect.get(t, prop, receiver);
},
set(t, prop, value, receiver) {
console.log(`set ${String(prop)} = ${value}`);
return Reflect.set(t, prop, value, receiver);
},
});
console.log(audited.name); // issoh
audited.name = "tech"; // set name = tech
audited.nme; // TypeError: 未定義のプロパティ: nme
トラップの中ではt[prop]と直接書かずReflect.getを使います。receiverを引き継がないと、ゲッターの中のthisがプロキシでなく元のオブジェクトを指してしまうためです。Vue 3のリアクティブシステムもこの仕組みで変更を検知しています。
クラスやメソッドに付けるデコレータ構文は、2026年10月時点でTC39のStage 2.7にあり、まだ言語仕様には入っていません(tc39/proposal-decorators)。TypeScriptは5.0で、--experimentalDecoratorsを付けなくてもこの提案に沿ったデコレータを書けるようにしました(TypeScript 5.0リリースノート)。
C++のテンプレートメタプログラミングとconstexpr
C++のメタプログラミングはコンパイル時に動きます。生成されたコードは通常のコードと同じく型検査されます。コンパイル時に完了した計算は実行時に繰り返す必要がありませんが、生成された関数などの実行コストは残ります。以下はApple clang 21.0.0の-std=c++20でコンパイル・実行しました。
テンプレートの再帰的実体化による計算
テンプレートメタプログラミング(TMP)は、テンプレートの再帰的な実体化を計算に使う技法です。C++標準化委員会のサンディエゴ会合で、Erwin Unruhがコンパイル中に素数を計算し、コンパイラの出力に並べるプログラムを書いたのがよく知られた起点です(本人による解説「Primzahlen in C++」)。
template <unsigned N> struct Factorial {
static const unsigned long long value = N * Factorial<N - 1>::value;
};
template <> struct Factorial<0> { static const unsigned long long value = 1; };
static_assert(Factorial<10>::value == 3628800); // コンパイル時に検査される
Factorial<0>の特殊化が再帰の終了条件です。この書き方は今でも型を操作する場面(型リストの変換など)で使われますが、値の計算には次のconstexprの方が読みやすくなります。
constexpr・consteval・if constexprの評価条件と使い分け
C++11のconstexpr関数は、定数式の条件を満たす呼び出しをコンパイル時に評価できます。ただし、引数が定数というだけではコンパイル時評価は保証されず、static_assertやconstexpr変数の初期化など、定数式を要求する文脈が必要です。C++20のconstevalはコンパイル時評価を強制し、実行時の値を渡すとコンパイルエラーにします。C++17のif constexprは、定数式の条件で分岐します。テンプレートの実体化時には、条件が確定すると選ばれなかった側は実体化されませんが、構文解析まで省略されるわけではありません。
#include <cstdio>
#include <type_traits>
constexpr unsigned long long factorial(unsigned n) { return n == 0 ? 1 : n * factorial(n - 1); }
consteval unsigned long long ct_factorial(unsigned n) { return factorial(n); }
template <class T> const char* kind() {
if constexpr (std::is_integral_v<T>) return "integral";
else if constexpr (std::is_floating_point_v<T>) return "floating";
else return "other";
}
static_assert(factorial(20) == 2432902008176640000ULL);
int main() {
std::printf("%llu %llu\n", factorial(20), ct_factorial(5)); // 2432902008176640000 120
std::printf("%s %s %s\n", kind<int>(), kind<double>(), kind<const char*>()); // integral floating other
}
ct_factorial(argc)のように実行時の値を渡すと、clangは「call to consteval function ‘ct_factorial’ is not a constant expression」で止まりました。計算をコンパイル時に終わらせたいことをコードで保証したい場合はconstevalを選びます。
C++26の静的リフレクション
これまでのC++には、構造体のメンバー名を列挙するような「型を調べる」機能がありませんでした。2025年6月のWG21ソフィア会合で静的リフレクション(P2996R13)をC++26の作業草案に入れることが可決され(WG21 N5016)、^^Tで型やメンバーの情報をコンパイル時の値として取り出せるようになります(P2996 Reflection for C++26)。enumと文字列の相互変換やシリアライザを、マクロや外部コード生成なしで書けるようになる変更です。2026年10月時点では、手元のApple clang 21.0.0は-freflectionを受け付けず、試すには対応したコンパイラが必要です。
Rustのマクロによるコード生成
Rustには、パターンに一致したトークン列を展開する宣言的マクロ(macro_rules!)と、トークン列を受け取ってRustのコードとして処理する手続き型マクロがあります。#[derive(Debug, Clone)]は組み込みのderiveマクロを呼び出し、構造体の定義から実装を生成します。独自のderiveマクロは手続き型マクロとして定義できます(The Rust Reference: Procedural Macros)。独自のderiveはRust 1.15で、属性マクロと関数風マクロは1.30で安定化しました(Rust 1.30.0のリリース告知)。
// 宣言的マクロ:同じ形のゲッターを並べて生成する
macro_rules! getters {
($t:ident { $($f:ident : $ty:ty),* $(,)? }) => {
impl $t { $(pub fn $f(&self) -> &$ty { &self.$f })* }
};
}
#[derive(Debug, Clone, PartialEq)]
struct User { name: String, age: u32 }
getters!(User { name: String, age: u32 });
fn main() {
let u = User { name: "issoh".into(), age: 10 };
let v = u.clone();
println!("{} {} {:?} {}", u.name(), u.age(), v, u == v);
// issoh 10 User { name: "issoh", age: 10 } true
}
rustc 1.99.1で実行した結果です。Rubyのdefine_methodと違い、生成されたメソッドはコンパイル時に型検査され、存在しないフィールド名を渡せばその場でエラーになります。展開結果を確かめたいときはcargo expand(cargo-expand)で見られます。
メタプログラミングを使うべきでない場面
メタプログラミングは「書く量」を減らす代わりに「読む量」と「調べる手間」を増やします。次の条件に当てはまるなら、素直に普通のコードで書くべきです。
呼び出しが高頻度のホットパス
Ruby 4.0.6で、値1を返す処理を4通りの方法でそれぞれ500万回呼んだ測定例です。以下は保存された1回分のログの値で、環境やJIT設定によって比率は変わります(Intel Core i9-9880H、Benchmark.bmの実時間)。
| 呼び出し方 | 実時間 | defとの比 |
|---|---|---|
| def で定義 | 0.64秒 | 1.0倍 |
| define_method で定義 | 0.86秒 | 1.4倍 |
| send(:plain) | 1.38秒 | 2.2倍 |
| method_missing 経由 | 3.24秒 | 5.1倍 |
method_missingは、通常のメソッド探索に失敗してから呼ばれるため遅くなります。何度も呼ばれる名前が決まっているなら、初回のmethod_missingでdefine_methodしてしまい、2回目以降は通常のメソッドとして呼ばれるようにするのが定石です。
動的生成による定義元の検索性の低下
define_method("#{key}=")で生成したメソッドは、ソースをport=で検索しても見つかりません。新しくチームに入った人が定義元を探せないコードは、保守のたびに時間を奪います。生成するなら、生成元の配列のそばにコメントで生成されるメソッド名を書く、RubyならRBS、Pythonなら型スタブで型情報を補うといった手当てが要ります。
AOTコンパイルや静的解析を前提にする環境
実行時メタプログラミングは、事前にコードを確定させたい仕組みと相性が悪くなります。たとえばRubyのAOTコンパイラSpinelでは、method_missingは定義しても呼ばれず、define_methodはリテラルの名前にしか対応せず、Refinementsも効きません(Spinelで書けないRubyを実測で整理した記事)。Railsがそのままコンパイルできないのも、メタプログラミングへの依存が深いためです。将来ネイティブ化やツリーシェイキングを考えるコードでは、実行時の動的定義を避けておくと選択肢が残ります。
使ってよい場面の目安
逆に、次のような場面では書く価値があります。同じ形のメソッドを、DBのカラムやAPIのスキーマなど外部の定義に追従させて生成するとき。フレームワークやライブラリとして、利用者が書いたクラスを受け取って振る舞いを足すとき。テストのモックのように、本番コードの外側でだけ使うとき。業務アプリの数箇所を数行短くするために使うと、読みにくさの代償の方が大きくなります。
既存システムの改修で、過去に入れたメタプログラミングが保守の障害になっている場合は、生成される定義を一覧化してから普通のコードに展開する作業が有効です。設計から見直す場合はフルスクラッチ開発でのご相談も受け付けています。
よくある質問
メタプログラミングとは何ですか?
クラス・メソッド・構文木・型などのプログラムそのものをデータとして扱い、生成・変更・検査するプログラムを書く技法です。Rubyのdefine_method、Pythonのデコレータ、C++のテンプレート、Rustのマクロが代表例です。
メタプログラムとは何ですか?
プログラムを生成したり書き換えたりするプログラムそのものを指します。メタプログラムを書く技法がメタプログラミングです。Railsのhas_manyのように、1行の宣言から複数のメソッドを定義するコードがその例です。
メタ言語とメタプログラミングの違いは何ですか?
メタ言語はBNFのように言語の文法や意味を記述するための言語で、メタプログラミングはプログラムを生成・変更するプログラムを書く技法です。前者は記述の道具、後者はコードを作る手段という違いがあります。
テンプレートメタプログラミングとは何ですか?
C++のテンプレートの再帰的な実体化を使い、コンパイル時に計算や型の変換を行う技法です。値の計算はC++11以降のconstexprで書く方が読みやすく、現在は型の操作に使われることが多くなっています。
メタプログラミングとリフレクションの違いは何ですか?
リフレクションは実行時に型やメソッドを調べたり呼び出したりする機能で、メタプログラミングを実現する部品の一つです。メタプログラミングはそれに加え、コンパイル時のコード生成(マクロ・テンプレート)も含む広い概念です。