Rustは、安全で高速なシステムプログラミングを実現するために、関数を第一級オブジェクトとして扱う柔軟な言語機能を備えています。
関数を他の関数に渡したり、データ構造の中に格納したりする際、中心的な役割を果たすのが「関数ポインタ」です。
関数ポインタは fn というキーワードで表現され、プログラム内で定義された関数のメモリアドレスを指し示します。
初心者にとってはクロージャとの違いが分かりにくい部分ですが、関数ポインタ特有の性質を理解することは、メモリ効率の良いプログラムを書く上で不可欠です。
本記事では、2026年現在の最新のRust言語仕様に基づき、関数ポインタの定義からクロージャとの使い分け、実践的な活用シーンまで詳しく解説します。
Rustにおける関数ポインタ(fn)の基礎
Rustの関数ポインタは、特定のシグネチャを持つ関数への参照を保持するためのプリミティブ型です。
関数ポインタの型は、引数の型と戻り値の型によって決定されます。
例えば、i32 型の引数を一つ取り、i32 型を返す関数のポインタ型は fn(i32) -> i32 と記述されます。
関数ポインタは、通常の参照(&T)とは異なり、直接関数の実行コードが配置されているアドレスを指すのが特徴です。
関数ポインタの宣言と代入
具体的なコード例を見て、関数ポインタがどのように定義され、利用されるかを確認しましょう。
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
// 関数ポインタ型を明示的に指定して変数に代入
let f: fn(i32) -> i32 = add_one;
let result = f(5);
println!("Result: {}", result);
}
Result: 6
この例では、add_one という関数を f という変数に代入しています。
この f は、実行時に add_one のアドレスを保持しており、通常の関数と同様に呼び出すことが可能です。
関数アイテムと関数ポインタの決定的な違い
Rustにおける関数には、実は「関数アイテム」と「関数ポインタ」という2つの異なる概念が存在します。
この違いを理解することが、Rustの型システムを使いこなす第一歩となります。
関数アイテムはゼロサイズ型である
Rustで fn foo() {} と定義したとき、foo そのものは「関数アイテム」と呼ばれる特殊な型を持ちます。
関数アイテムは、コンパイル時に特定の関数を指すことが確定しているため、実行時にはメモリを一切消費しない「ゼロサイズ型(ZST)」として扱われます。
これにより、コンパイラはインライン化などの強力な最適化を適用することが可能になります。
関数ポインタへの型強制(Coercion)
一方で、関数アイテムは必要に応じて fn 型(関数ポインタ)へと自動的に型強制されます。
関数ポインタは実行時にどのアドレスを指すかが変わる可能性があるため、ポインタサイズ(通常は64ビット)のメモリを消費します。
以下の表に、両者の主な違いをまとめました。
| 特徴 | 関数アイテム | 関数ポインタ (fn) |
|---|---|---|
| サイズ | 0 バイト | 1 ポインタサイズ (8バイト等) |
| 最適化 | 強力なインライン化が可能 | 動的な呼び出し(オーバーヘッドあり) |
| 用途 | 通常の関数定義と直接利用 | 実行時の関数切り替え、FFI |
関数ポインタとクロージャの比較と使い分け
関数を引数に取る関数を定義する場合、関数ポインタ fn を使うべきか、クロージャトレイト(Fn, FnMut, FnOnce)を使うべきか迷うことが多くあります。
この2つの最大の違いは、「環境をキャプチャできるかどうか」にあります。
状態を保持できるかどうか
クロージャは、定義されたスコープ内の変数を「キャプチャ」して保持することができます。
これに対し、関数ポインタ fn は純粋に関数のアドレスだけを指すため、外部の変数にアクセスすることはできません。
fn main() {
let x = 10;
// クロージャは外部変数 x をキャプチャできる
let closure = |y| y + x;
// 関数ポインタは外部変数をキャプチャできない
// let f_ptr: fn(i32) -> i32 = |y| y + x; // コンパイルエラー
}
上記のコードで、関数ポインタに x をキャプチャしたクロージャを代入しようとすると、コンパイルエラーが発生します。
逆に、何もキャプチャしていないクロージャは、関数ポインタ fn へと型強制することが可能です。
パフォーマンスとジェネリクスの観点
関数を引数に取る際、fn 型を直接指定すると、その引数は常にポインタサイズになります。
一方、ジェネリクスと Fn トレイト境界を使用すると、コンパイル時に関数ごとに「単態化(Monomorphization)」が行われます。
パフォーマンスを最優先し、コンパイル時の最適化を最大限引き出したい場合は Fn トレイトを使用するのが一般的です。
しかし、コンパイル後のバイナリサイズを小さく抑えたい場合や、実行時に多くの異なる関数を同一の型として扱いたい場合は、関数ポインタ fn が適しています。
FFI(外部関数インターフェース)における活用
Rustの関数ポインタが不可欠となる重要なシーンの一つが、C言語などの他言語との連携(FFI)です。
C言語の関数はクロージャという概念を持たないため、コールバック関数を渡す際には関数ポインタを使用する必要があります。
Rustの fn 型は、C言語の関数ポインタとバイナリレベルで互換性があるため、安全に外部ライブラリと通信できます。
extern "C" {
// C言語側の関数が関数ポインタを要求する場合
fn register_callback(cb: extern "C" fn(i32));
}
extern "C" fn my_callback(val: i32) {
println!("C code called Rust with: {}", val);
}
fn main() {
unsafe {
// Rustで定義した関数をC側に渡す
register_callback(my_callback);
}
}
このように、extern "C" fn という形式で宣言された関数ポインタは、言語の境界を越えて関数を受け渡しする際の標準的な手段となります。
実践的な実装パターンとコード例
関数ポインタをより効果的に活用するパターンとして、関数のディスパッチテーブル(ルックアップテーブル)の作成があります。
特定のキーに基づいて実行する関数を切り替える場合、match 文を使う代わりに HashMap などに関数ポインタを格納しておくと、コードが非常にスッキリします。
use std::collections::HashMap;
type CommandFn = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn subtract(a: i32, b: i32) -> i32 { a - b }
fn multiply(a: i32, b: i32) -> i32 { a * b }
fn main() {
let mut commands: HashMap<&str, CommandFn> = HashMap::new();
commands.insert("add", add);
commands.insert("sub", subtract);
commands.insert("mul", multiply);
let op = "add";
if let Some(f) = commands.get(op) {
let res = f(10, 5);
println!("Result of {}: {}", op, res);
}
}
Result of add: 15
このパターンでは、すべての関数が同じ fn(i32, i32) -> i32 という型を持っているため、コレクションにまとめて格納することができます。
クロージャを使用する場合、各クロージャは独自の型を持つため、このように単純に HashMap に格納するには Box<dyn Fn...> といった動的ディスパッチが必要になります。
これに対し、関数ポインタ fn を使えば、ヒープ割当(Box)を避けつつ、関数を動的に選択できるという大きなメリットが得られます。
まとめ
Rustの関数ポインタ(fn)は、関数のアドレスを直接扱うためのシンプルかつ強力なツールです。
関数アイテムとは異なり、実行時にメモリを消費する代わりに関数を柔軟に変数として扱うことができます。
クロージャとの使い分けにおいては、「状態(環境)をキャプチャする必要があるか」が最大の判断基準となります。
状態を持たない純粋なロジックを渡す場合や、C言語との連携が必要な場合、またはメモリ効率を極限まで高めたい場合には、関数ポインタの利用が最適です。
Rustの強力な型システムの中で、関数ポインタの特性を正しく理解し、クロージャやジェネリクスと適切に使い分けていきましょう。
今回解説したメモリレイアウトや型強制の知識は、より高度なRustプログラミングやパフォーマンスチューニングにおいて必ず役立つはずです。
