C++ 関数ポインタの構文と実践的な活用方法

ポインタ型の基礎

ポインタ変数を宣言する際、その型はポインタが指し示す対象のデータ型を定義します。ポインタの型と、それが参照するオブジェクトの型は厳密に一致する必要があります。

int data = 42;
// ptr は int 型変数 data のアドレスを保持します
int *ptr = &data;

関数ポインタの定義

関数ポインタも他のポインタと同様に、特定の型を持ちます。関数の型は戻り値の型とパラメータのリストによって決定され、関数名自体は型には含まれません。関数を指すポインタを宣言するには、関数名の部分をポインタ宣言に置き換えます。

// 対象となる関数
bool validateString(const std::string&, const std::string&);

// funcPtr は関数ポインタです
// 指す関数の型:bool (const std::string&, const std::string&)
bool (*funcPtr)(const std::string&, const std::string&);

// 注意点:以下の宣言は bool 型のポインタを返す関数となります
bool *funcPtr(const std::string&, const std::string&);

宣言の解釈は識別子から開始します。funcPtr の直前に*があるためポインタであり、右側にパラメータリストがあるため関数を指しています。左側の bool が戻り値の型となります。

関数ポインタの運用

関数名を値として使用すると、自動的にその関数のアドレス(ポインタ)に変換されます。したがって、代入時にアドレス演算子&は省略可能です。

bool (*funcPtr)(const std::string&, const std::string&);

// 以下の 2 行は同等です
funcPtr = validateString;
funcPtr = &validateString;

関数ポインタを通じて関数を呼び出す場合、明示的に逆参照を行う必要はありません。

// 逆参照なしで呼び出し
bool result1 = funcPtr("hello", "world");
// 逆参照ありで呼び出し(同等)
bool result2 = (*funcPtr)("hello", "world");
// 関数名直接使用(同等)
bool result3 = validateString("hello", "world");

関数ポインタに nullptr または 0 を代入することで、どの関数も指していない状態を表せます。また、ポインタの型 signature は指向する関数と完全に一致していなければなりません。

std::string::size_type measureLength(const std::string&, const std::string&);
bool compareCStr(const char*, const char*);
bool validateString(const std::string&, const std::string&);

bool (*funcPtr)(const std::string&, const std::string&);

funcPtr = nullptr;          // OK: 無効化
funcPtr = validateString;   // OK: 型一致
funcPtr = compareCStr;      // NG: パラメータ型不一致
funcPtr = measureLength;    // NG: 戻り値型不一致

オーバーロードされた関数のポインタ

関数がオーバーロードされている場合、ポインタの型によってどの関数を指すかが解決されます。ポインタのシグネチャは、オーバーロード候補のいずれかと完全に一致する必要があります。

void handleData(int*);
void handleData(unsigned int);

// OK: void handleData(unsigned int) にマッチ
void (*handler1)(unsigned int) = handleData;
// NG: パラメータ型が一致しない
void (*handler2)(int) = handleData;
// NG: 戻り値型が一致しない
double (*handler3)(int*) = handleData;

関数ポインタをパラメータとする

関数型そのものをパラメータにすることはできませんが、関数ポインタをパラメータにすることは可能です。宣言上は関数型のように記述しても、コンパイラは自動的にポインタ型として扱います。

// 関数型のように記述(内部ではポインタに変換)
void processWithCallback(const std::string &s1, const std::string&s2, bool cmp(const std::string&, const std::string&));

// 明示的にポインタとして記述
void processWithCallback(const std::string &s1, const std::string&s2, bool (*cmp)(const std::string&, const std::string&));

呼び出し側では関数名をそのまま渡せます。

processWithCallback(str1, str2, validateString);

関数ポインタの宣言は複雑になりがちです。タイプエイリアスや decltype を利用することで可読性を向上させられます。typedef または using 宣言で型に名前を付けましょう。

// 関数型のエイリアス
typedef bool FuncType(const std::string&, const std::string&);
// decltype で関数型を取得(ポインタではない)
typedef decltype(validateString) FuncType2;

// 関数ポインタ型のエイリアス
typedef bool (*FuncPtrType)(const std::string&, const std::string&);
// decltype で取得した型に*を付けてポインタ化
typedef decltype(validateString) *FuncPtrType2;

これらを用いてパラメータを簡略化できます。

void processWithCallback(const std::string &s1, const std::string&s2, FuncType);
void processWithCallback(const std::string &s1, const std::string&s2, FuncPtrType2);

関数ポインタを戻り値とする

関数そのものを返すことはできませんが、関数を指すポインタを戻り値とすることは可能です。戻り値の型は明示的にポインタとして指定する必要があります。

1. タイプエイリアスの利用

これが最も推奨される方法です。

using F = int(int*, int);
using PF = int(*)(int*, int);

PF getHandler(int);      // OK: 関数ポインタを返す
F getHandler(int);       // NG: 関数型そのものは返せない
F *getHandler(int);      // OK: 明示的にポインタ指定

2. 直接宣言

識別子から外側へ読み解きます。getHandler は関数であり、戻り値はポインタで、そのポインタは関数を指します。

int(*getHandler(int))(int*, int);

3. トレイリング戻り値型

複雑な戻り値型を持つ場合に有効です。auto をプレースホルダーとして使用し、-> の後に実際の型を記述します。

auto getHandler(int) -> int(*)(int*, int);

decltype による関数ポインタ型の取得

戻り値として特定の関数ポインタを返す場合、decltype を使用して型を推論させることができます。decltype は関数型を返すため、ポインタ型にするには*を付与する必要があります。

std::string::size_type measureLength(const std::string&, const std::string&);
std::string::size_type calcLarger(const std::string&, const std::string&);

// measureLength の型を取得し、ポインタとして返す
decltype(measureLength) *getComparator(const std::string&);

実践演習

演習 1

2 つの int を受け取り int を返す関数を宣言し、その関数ポインタを要素とする vector を定義してください。

int operation(int, int);
// decltype で関数型を取得し、*でポインタ化して vector の要素型とする
std::vector<decltype(operation)*> registry;

演習 2

加算、減算、乗算、除算を行う 4 つの関数を作成し、前述の vector に格納してください。

int opAdd(int a, int b) { return a + b; }
int opSub(int a, int b) { return a - b; }
int opMul(int a, int b) { return a * b; }
int opDiv(int a, int b) { return b != 0 ? a / b : 0; }

// 関数名は自動的にポインタに変換されて格納される
registry.push_back(opAdd);
registry.push_back(opSub);
registry.push_back(opMul);
registry.push_back(opDiv);

演習 3

vector に格納された各関数ポインタを呼び出し、結果を出力してください。

std::vector<decltype(operation)*> registry{ opAdd, opSub, opMul, opDiv };

// 関数ポインタは直接呼び出し可能(逆参照不要)
for (auto func : registry) {
    std::cout << func(10, 5) << std::endl;
}

タグ: C++ 関数ポインタ,typedef decltype std::vector,ポインタ構文

7月26日 20:00 投稿