C++の関数ポインタとは、特定の型を持つ関数を指し、その関数を間接的に呼び出すためのポインタです。
通常のポインタが変数やオブジェクトを指すのに対し、関数ポインタは実行可能な関数を指します。
関数ポインタを利用すると、実行する関数を途中で切り替えたり、関数を別の関数へ引数として渡したりできます。
主に、次のような場面で使われます。
- コールバック関数を登録する場合。
- 条件に応じて処理を切り替える場合。
- 複数の関数を配列で管理する場合。
- C言語形式のAPIへ関数を渡す場合。
- メニューやコマンドと処理を対応させる場合。
関数ポインタの基本的な仕組み
通常の関数を定義する
まず、2つの整数を受け取り、その合計を返す関数を定義します。
int add(int a, int b)
{
return a + b;
}
この関数には、次の型情報があります。
- 戻り値の型は
int。 - 第1引数の型は
int。 - 第2引数の型は
int。
この関数を指す関数ポインタも、原則として同じ戻り値と引数の型を持つ必要があります。
関数ポインタを宣言する
add関数を指せる関数ポインタは、次のように宣言します。
int (*funcPtr)(int, int);
この宣言は、次の要素で構成されています。
int // 戻り値の型
(*funcPtr) // funcPtrはポインタ
(int, int) // 2つのint型引数を受け取る
つまり、funcPtrは「2つのint型引数を受け取り、int型の値を返す関数」を指すポインタです。
関数ポインタに関数を代入する
関数ポインタには、関数名をそのまま代入できます。
funcPtr = add;
アドレス演算子&を付けて、次のように書くこともできます。
funcPtr = &add;
通常の代入や引数渡しでは、関数名が関数ポインタへ暗黙的に変換されます。
そのため、一般的には&を省略した書き方が使われます。
funcPtr = add;
関数ポインタから関数を呼び出す
関数ポインタに関数を代入した後は、次のように呼び出せます。
int result = funcPtr(10, 20);
間接参照演算子*を使い、次のように書くこともできます。
int result = (*funcPtr)(10, 20);
どちらも同じ関数を呼び出します。
通常は、簡潔で読みやすい次の形式が使われます。
funcPtr(10, 20);
関数ポインタの基本的な使用例
関数を代入して呼び出す
次のプログラムでは、add関数を関数ポインタに代入して呼び出しています。
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int main()
{
int (*funcPtr)(int, int) = add;
int result = funcPtr(10, 20);
std::cout << result << '\n';
return 0;
}
実行結果は次のとおりです。
30
処理の流れは、次のようになります。
add関数を定義する。funcPtrという関数ポインタを宣言する。funcPtrにadd関数を代入する。funcPtr(10, 20)を通してadd(10, 20)を呼び出す。
関数ポインタの宣言で丸括弧が必要な理由
丸括弧がある宣言
関数ポインタは、次のように宣言します。
int (*funcPtr)(int, int);
*funcPtrが丸括弧で囲まれているため、funcPtrがポインタであると解釈されます。
丸括弧がない宣言
次の宣言は、関数ポインタではありません。
int *funcPtr(int, int);
これは、「2つのint型引数を受け取り、int*型を返す関数」の宣言です。
2つを比較すると、次のようになります。
int (*funcPtr)(int, int); // intを返す関数へのポインタ
int *funcPtr(int, int); // intへのポインタを返す関数
関数呼び出しを表す()は、宣言の中で*よりも強く結び付きます。
そのため、関数ポインタを宣言するときは、*funcPtrを丸括弧で囲む必要があります。
関数ポインタで処理を切り替える方法
複数の関数を用意する
戻り値と引数の型が一致する関数であれば、同じ関数ポインタへ代入できます。
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int main()
{
int (*operation)(int, int) = nullptr;
operation = add;
std::cout << operation(10, 5) << '\n';
operation = subtract;
std::cout << operation(10, 5) << '\n';
operation = multiply;
std::cout << operation(10, 5) << '\n';
return 0;
}
実行結果は次のとおりです。
15
5
50
operationが指す関数を変更することで、同じ呼び出し方のまま処理内容を切り替えられます。
operation(10, 5);
この仕組みは、計算方法や動作モードを実行時に切り替えたい場合に便利です。
関数ポインタを引数として渡す方法
コールバック関数として利用する
関数ポインタは、別の関数の引数として受け取れます。
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int calculate(
int a,
int b,
int (*operation)(int, int)
)
{
return operation(a, b);
}
int main()
{
std::cout << calculate(10, 5, add) << '\n';
std::cout << calculate(10, 5, subtract) << '\n';
return 0;
}
実行結果は次のとおりです。
15
5
calculate関数は、具体的な計算方法を自分では決めていません。
第3引数として渡された関数を呼び出し、実際の計算を行います。
このように、別の処理へ渡され、その処理の中から呼び出される関数を、一般にコールバック関数と呼びます。
コールバック関数のメリット
コールバック関数を利用すると、共通処理と、状況によって変わる処理を分離できます。
たとえば、次のような関数を定義できます。
void processData(void (*callback)(int));
processData側はデータの読み込みや変換を担当し、処理結果をどのように扱うかはcallback側へ任せられます。
ただし、関数ポインタがnullptrになる可能性がある設計では、呼び出す前に有効性を確認する必要があります。
usingで関数ポインタ型に別名を付ける方法
usingを使った型エイリアス
関数ポインタの型は、そのまま書くと複雑になりやすいという特徴があります。
int (*operation)(int, int);
C++では、usingを使って関数ポインタ型に分かりやすい名前を付けられます。
using Operation = int (*)(int, int);
この型を使うと、関数ポインタを次のように宣言できます。
Operation operation = add;
使用例は次のとおりです。
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int calculate(int a, int b, Operation operation)
{
return operation(a, b);
}
int main()
{
Operation operation = add;
std::cout << calculate(10, 20, operation) << '\n';
return 0;
}
複雑な関数ポインタ型を何度も使う場合は、usingで型名を定義するとコードが読みやすくなります。
typedefで関数ポインタ型に別名を付ける方法
typedefを使った書き方
従来のC++では、typedefを使って関数ポインタ型に名前を付ける方法もあります。
typedef int (*Operation)(int, int);
使い方はusingの場合と同じです。
Operation operation = add;
ただし、現代的なC++では、一般にusingのほうが構文を読み取りやすいとされています。
using Operation = int (*)(int, int);
なお、typedefが非推奨になっているわけではありません。
既存のコードやC言語との互換性を意識したコードでは、現在でも使われています。
関数ポインタの配列を作る方法
複数の関数を配列で管理する
同じ型の関数ポインタは、配列に格納できます。
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
using Operation = int (*)(int, int);
int main()
{
Operation operations[] = {
add,
subtract,
multiply
};
std::cout << operations[0](10, 5) << '\n';
std::cout << operations[1](10, 5) << '\n';
std::cout << operations[2](10, 5) << '\n';
return 0;
}
実行結果は次のとおりです。
15
5
50
番号によって処理を選択する
関数ポインタの配列を使うと、番号に応じて実行する処理を選択できます。
#include <iostream>
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
using Operation = int (*)(int, int);
int main()
{
Operation operations[] = {
add,
subtract,
multiply
};
int choice = 0;
std::cout << "0: 加算、1: 減算、2: 乗算\n";
std::cin >> choice;
if (choice < 0 || choice >= 3)
{
std::cout << "無効な番号です。\n";
return 1;
}
std::cout << operations[choice](10, 5) << '\n';
return 0;
}
このような仕組みは、メニュー処理やコマンド処理の実装に利用できます。
実務では、配列の要素数を直接数値で書かず、std::arrayやstd::sizeを使うと保守しやすくなります。
ラムダ式を関数ポインタに代入する方法
キャプチャを持たないラムダ式
キャプチャを持たないラムダ式は、条件を満たせば対応する関数ポインタ型へ変換できます。
#include <iostream>
int main()
{
int (*operation)(int, int) = [](int a, int b)
{
return a + b;
};
std::cout << operation(10, 20) << '\n';
return 0;
}
このラムダ式は外部の変数をキャプチャしていないため、関数ポインタへ変換できます。
ただし、ラムダ式の引数型と戻り値型は、代入先の関数ポインタ型と一致している必要があります。
キャプチャを持つラムダ式
次のようなキャプチャ付きラムダ式は、通常の関数ポインタへ変換できません。
int value = 10;
int (*func)(int) = [value](int x)
{
return x + value;
};
このコードはコンパイルエラーになります。
キャプチャ付きラムダ式は、外部変数の値や参照を内部状態として保持する必要があります。
単純な関数ポインタは関数を指す情報しか保持できないため、キャプチャの状態を表現できません。
キャプチャ付きラムダ式を保存したい場合は、std::functionやテンプレートを利用します。
関数ポインタとstd::functionの違い
関数ポインタの特徴
関数ポインタは、主に通常の関数、staticメンバ関数、キャプチャを持たないラムダ式を扱えます。
using Operation = int (*)(int, int);
主な特徴は次のとおりです。
- 構造が単純。
- C言語形式のAPIと連携しやすい。
- 関数の型が明確。
- キャプチャ付きラムダ式は保存できない。
- 保存できる呼び出し対象の種類が限られる。
std::functionの特徴
std::functionは、関数、ラムダ式、関数オブジェクトなど、さまざまな呼び出し可能オブジェクトを保存できます。
#include <functional>
#include <iostream>
int main()
{
int value = 10;
std::function<int(int)> func = [value](int x)
{
return x + value;
};
std::cout << func(5) << '\n';
return 0;
}
実行結果は次のとおりです。
15
std::functionは型消去を行うため、関数ポインタと比べて呼び出しやオブジェクト管理のコストが増える可能性があります。
格納するオブジェクトの大きさや処理系の実装によっては、動的メモリ確保が発生する場合もあります。
ただし、std::functionが必ず動的メモリ確保を行うわけではありません。
単純な関数だけを扱う場合や、C言語形式のAPIと連携する場合は、関数ポインタが適しています。
さまざまな呼び出し可能オブジェクトを統一的に扱いたい場合は、std::functionが便利です。
関数ポインタとメンバ関数ポインタの違い
通常の関数ポインタ
クラスに属さない自由関数や、staticメンバ関数は、通常の関数ポインタで扱えます。
int (*func)(int, int);
非staticメンバ関数ポインタ
非staticメンバ関数は、特定のオブジェクトに対して呼び出す必要があります。
そのため、通常の関数ポインタとは異なるメンバ関数ポインタを使用します。
#include <iostream>
class Calculator
{
public:
int add(int a, int b)
{
return a + b;
}
};
int main()
{
int (Calculator::*func)(int, int) = &Calculator::add;
Calculator calculator;
int result = (calculator.*func)(10, 20);
std::cout << result << '\n';
return 0;
}
メンバ関数ポインタの宣言は、次のとおりです。
int (Calculator::*func)(int, int);
オブジェクトから呼び出す場合は、.*演算子を使います。
(calculator.*func)(10, 20);
オブジェクトへのポインタから呼び出す場合は、->*演算子を使います。
Calculator* calculatorPtr = &calculator;
int result = (calculatorPtr->*func)(10, 20);
非staticメンバ関数ポインタは、通常の関数ポインタとは別の型です。
constメンバ関数を指す場合
対象のメンバ関数がconstの場合は、メンバ関数ポインタ型にもconstを付ける必要があります。
class Calculator
{
public:
int add(int a, int b) const
{
return a + b;
}
};
int (Calculator::*func)(int, int) const = &Calculator::add;
constの有無は、メンバ関数ポインタの型に影響します。
staticメンバ関数を関数ポインタで扱う方法
通常の関数ポインタに代入する
staticメンバ関数は、特定のオブジェクトに依存せず呼び出せます。
そのため、通常の関数ポインタへ代入できます。
#include <iostream>
class Calculator
{
public:
static int add(int a, int b)
{
return a + b;
}
};
int main()
{
int (*func)(int, int) = Calculator::add;
std::cout << func(10, 20) << '\n';
return 0;
}
非staticメンバ関数には、呼び出し対象となるオブジェクトが必要です。
一方、staticメンバ関数にはオブジェクトが必要ないため、通常の関数ポインタで扱えます。
関数ポインタをnullptrで初期化する方法
未設定の状態を表す
関数ポインタにまだ関数を設定していない場合は、nullptrで初期化できます。
int (*func)(int, int) = nullptr;
nullptrは、その関数ポインタが有効な関数を指していないことを表します。
呼び出す前に確認する
nullptrの関数ポインタを呼び出すと、未定義動作になります。
そのため、呼び出す前に確認する必要があります。
if (func != nullptr)
{
int result = func(10, 20);
}
次のように簡潔に書くこともできます。
if (func)
{
int result = func(10, 20);
}
関数ポインタが外部から渡される場合や、設定が任意の場合は、有効性を確認することが重要です。
関数ポインタの型を一致させる必要性
戻り値と引数の型を合わせる
関数ポインタに代入する関数は、原則として戻り値と引数の型が一致していなければなりません。
たとえば、次の関数を考えます。
double divide(double a, double b)
{
return a / b;
}
この関数は、次の関数ポインタへ代入できます。
double (*func)(double, double) = divide;
一方、次の型には代入できません。
int (*func)(int, int) = divide;
戻り値と引数の型が異なるため、互換性がありません。
異なる型の関数を無理にキャストして呼び出すと、未定義動作を引き起こす可能性があります。
関数ポインタでは、対象となる関数の型を正確に確認する必要があります。
オーバーロードされた関数を代入する方法
代入先の型で関数を特定する
C++では、同じ名前で引数型の異なる関数を定義できます。
int calculate(int a, int b)
{
return a + b;
}
double calculate(double a, double b)
{
return a + b;
}
この場合、代入先の関数ポインタ型によって、どちらの関数を使用するかが決まります。
int (*intFunc)(int, int) = calculate;
double (*doubleFunc)(double, double) = calculate;
static_castで明示的に指定する
型だけでは対象を決定しにくい場合は、static_castを使って明示的に指定できます。
auto func =
static_cast<int (*)(int, int)>(calculate);
ただし、代入先の型が明確であれば、通常はキャストする必要はありません。
戻り値がvoidの関数ポインタ
引数を持たないvoid関数
値を返さない関数も、関数ポインタで扱えます。
#include <iostream>
void showMessage()
{
std::cout << "処理を実行しました。\n";
}
int main()
{
void (*func)() = showMessage;
func();
return 0;
}
引数を持つvoid関数
引数を受け取るvoid関数の場合は、次のように宣言します。
#include <iostream>
void printValue(int value)
{
std::cout << value << '\n';
}
int main()
{
void (*func)(int) = printValue;
func(100);
return 0;
}
noexceptと関数ポインタの関係
noexcept付き関数ポインタ
C++17以降では、例外指定が関数型の一部として扱われます。
次の関数は、例外を送出しないことを表しています。
void safeFunction() noexcept
{
}
noexcept付き関数専用の関数ポインタは、次のように宣言できます。
void (*func)() noexcept = safeFunction;
例外を送出する可能性のある通常の関数を、noexcept付き関数ポインタへ代入することはできません。
一方、noexcept付き関数を、noexceptのない関数ポインタへ代入することは可能です。
void (*func)() = safeFunction;
入門的なコードでは意識する機会が少ないものの、関数型を厳密に扱う場合は重要な違いです。
autoで関数ポインタ型を推論する方法
関数から型を自動推論する
関数ポインタ型を手動で書かず、autoを使うこともできます。
int add(int a, int b)
{
return a + b;
}
int main()
{
auto func = &add;
int result = func(10, 20);
}
この場合、funcの型は次のように推論されます。
int (*)(int, int)
&を省略しても、通常は同じ関数ポインタ型になります。
auto func1 = add;
auto func2 = &add;
既存の関数を単純に保存する場合は、autoを使うと宣言を簡潔にできます。
一方、公開APIや関数の引数型として使用する場合は、usingで明示的な型名を付けたほうが意図を伝えやすい場合があります。
decltypeで関数の型を取得する方法
関数型と関数ポインタ型の違い
decltypeを使うと、関数から型を取得できます。
int add(int a, int b)
{
return a + b;
}
using AddFunction = decltype(add);
using AddPointer = decltype(&add);
この2つは異なる型です。
decltype(add) // 関数型
decltype(&add) // 関数ポインタ型
関数ポインタ変数を宣言する場合は、次のように書けます。
decltype(&add) func = add;
関数型そのものを通常の変数型として使うことはできませんが、関数への参照として使うことは可能です。
decltype(add)& funcRef = add;
関数そのものと関数ポインタの違い
関数型
次のような関数を考えます。
int add(int, int);
addそのものの型は、概念的には次のような関数型です。
int(int, int)
関数ポインタ型
&addの型は、次のような関数ポインタ型です。
int (*)(int, int)
関数型の変数を、通常の変数と同じように作ることはできません。
一方、関数ポインタ型の変数は作成できます。
int (*func)(int, int) = add;
多くの式では、関数名が自動的に関数ポインタへ変換されます。
そのため、通常のコードでは両者の違いを強く意識しないこともあります。
関数ポインタを返す関数
usingを使って戻り値型を簡潔にする
関数ポインタは、関数の戻り値として返すこともできます。
ただし、宣言が複雑になりやすいため、usingを使う方法が分かりやすいでしょう。
#include <iostream>
using Operation = int (*)(int, int);
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
Operation selectOperation(char symbol)
{
if (symbol == '+')
{
return add;
}
if (symbol == '-')
{
return subtract;
}
return nullptr;
}
int main()
{
Operation operation = selectOperation('+');
if (operation)
{
std::cout << operation(10, 5) << '\n';
}
return 0;
}
この例では、selectOperation関数が、指定された記号に対応する関数ポインタを返しています。
対象となる関数がない場合はnullptrを返すため、呼び出し側で有効性を確認しています。
関数ポインタを使う際の注意点
関数の型を正確に合わせる
戻り値や引数の型が異なる関数は、同じ関数ポインタへ代入できません。
using Operation = int (*)(int, int);
この型には、原則として「2つのint型引数を受け取り、int型の値を返す関数」を代入します。
nullptrを呼び出さない
何も指していない関数ポインタを呼び出してはいけません。
Operation operation = nullptr;
if (operation)
{
operation(10, 20);
}
無効になった関数ポインタを使わない
名前付きの自由関数やメンバ関数は、ローカル変数のようにブロックを抜けたことで消滅するものではありません。
そのため、通常の関数を指す関数ポインタでは、ローカル変数の寿命切れと同じ問題は基本的に発生しません。
ただし、動的ライブラリから取得した関数ポインタは、そのライブラリをアンロードすると無効になります。
アンロード後の関数ポインタを呼び出してはいけません。
非staticメンバ関数と混同しない
非staticメンバ関数は、通常の関数ポインタでは扱えません。
int (*func)(int, int);
非staticメンバ関数には、クラス名を含む専用の型が必要です。
int (Calculator::*func)(int, int);
不要なキャストを避ける
異なる型の関数を無理に関数ポインタへキャストすると、正常に呼び出せる保証がありません。
auto func =
reinterpret_cast<int (*)(int, int)>(someFunction);
このようなコードは、プラットフォーム固有の低レベル処理など、特別な理由がない限り避けるべきです。
関数ポインタと仮想関数の使い分け
関数ポインタが向いている場面
関数ポインタは、呼び出す関数そのものを選択して保持する仕組みです。
単純な処理の切り替えや、C言語形式のAPIとの連携に向いています。
たとえば、計算方法やコールバック処理を切り替えるだけであれば、関数ポインタを使うと簡潔に実装できます。
仮想関数が向いている場面
仮想関数は、基底クラスのインターフェースを通して、実行時のオブジェクト型に対応するオーバーライドを呼び出す仕組みです。
複数の関連するデータと処理をオブジェクトとしてまとめたい場合や、継承による多態性を実現したい場合に向いています。
関数ポインタと仮想関数は、どちらも実行時に処理を切り替えられますが、目的や設計思想が異なります。
現代的なC++における使い分け
関数ポインタが適しているケース
次のような場合は、関数ポインタが適しています。
- C言語形式のライブラリへコールバックを渡す場合。
- 通常の関数だけを扱う場合。
- 呼び出し対象の型を単純に保ちたい場合。
- 低レベルなAPIとの互換性が必要な場合。
std::functionが適しているケース
次のような場合は、std::functionが便利です。
- キャプチャ付きラムダ式を保存する場合。
- 関数、ラムダ式、関数オブジェクトを同じ型で扱う場合。
- コールバックの柔軟性を重視する場合。
テンプレートが適しているケース
呼び出し対象の具体的な型をコンパイル時に決定できる場合は、テンプレートを使う方法もあります。
template <typename Func>
int calculate(int a, int b, Func operation)
{
return operation(a, b);
}
使用例は次のとおりです。
int result = calculate(
10,
20,
[](int a, int b)
{
return a + b;
}
);
この方法は、通常の関数、関数ポインタ、キャプチャ付きラムダ式、関数オブジェクトなどを受け取れます。
また、コンパイラが呼び出し対象の具体的な型を把握できるため、インライン化などの最適化が行われやすくなります。
ただし、実際に最適化されるかどうかは、コンパイラやビルド設定によって異なります。
関数ポインタの書き方一覧
基本的な宣言
戻り値がintで、2つのint型引数を受け取る関数ポインタは、次のように宣言します。
int (*func)(int, int);
関数を代入する
func = add;
宣言と同時に初期化する
int (*func)(int, int) = add;
関数を呼び出す
int result = func(10, 20);
usingで型名を付ける
using Operation = int (*)(int, int);
nullptrで初期化する
Operation operation = nullptr;
関数の引数として受け取る
int calculate(int a, int b, Operation operation);
関数ポインタの配列を作る
Operation operations[] = {
add,
subtract,
multiply
};
まとめ
C++の関数ポインタは、特定の型を持つ関数を指し、その関数を間接的に呼び出すための仕組みです。
基本形は次のとおりです。
戻り値の型 (*変数名)(引数の型);
たとえば、2つのint型引数を受け取り、int型の値を返す関数ポインタは、次のように宣言します。
int (*func)(int, int);
関数ポインタを使うと、実行する関数の切り替え、コールバック処理、関数の配列化などが可能になります。
一方で、関数の戻り値と引数の型を正確に合わせることや、nullptrや無効な関数ポインタを呼び出さないことが重要です。
また、非staticメンバ関数を扱う場合は、通常の関数ポインタではなくメンバ関数ポインタを使用します。
現代的なC++では、用途に応じて関数ポインタ、std::function、ラムダ式、テンプレートを使い分けることで、読みやすく柔軟なコードを設計できます。
以上、C++の関数ポインタの使い方についてでした。
最後までお読みいただき、ありがとうございました。
