関数ポインタ型を混在させて運用する - C++ プログラミング

PROGRAM


関数ポインタ型を混在させて運用する

C++ では、関数ポインタを扱う方法がいくつかあります。

  • C 言語式の関数ポインタを使用する方法
  • std::function<> クラステンプレートを使用する方法
  • std::bind<> クラステンプレートを使用する方法
  • ラムダ関数を使用する方法

これらの詳細については 関数のポインタを使用するラムダ関数を使用するstd::bind を使って関数を特殊化する でお話しましたけど、ここでは、それらを変数を使って相互にやり取りする場合について見て行きます。

 

std::function が基本の受け皿

いろんな種類の関数ポインタを混ぜて扱う場合、いちばん柔軟な受け皿になるのが std::function です。

これを使うと、C 言語からの関数ポインタも、std::bind<> を使って整えた関数も、ラムダ関数も、全ての種類を織り交ぜて格納することができます。

std::function<int(int)> func;

 

func = isalpha;

func = std::bind(fseek, file, 0, std::placeholders::_1);

func = [](int a)->int { return a * 2; };

func = [&b](int a)->int { return a * b; };

このように、この例では std::function<int(int)> 型なので、引数が int 型のものを 1 つ採って戻り値として int 型を返すものであればどれでも代入できます。

 

C 言語式の関数ポインタ変数に代入する場合

C 言語でお馴染みの関数ポインタ変数を使う場合、代入できないものが出てきます。

普通に代入できるのは、普通の関数と、クロージャを伴わないラムダ式です。同じ引数と戻り値でも、std::function<> やクロージャを伴うラムダ式は代入できません。

int (*func)(int);

 

func = isalpha;

func = [](int a)->int { return a * 2; };

std::bind<> を使った場合やラムダ式でもクロージャを扱う場合のような付加情報を内包する関数は、C 言語の関数ポインタでは扱えないようなので注意です。

 

ちなみに具体的には、Xcode 4.6.2 や Visual Studio 2012 で std::bind を旧来の関数ポインタに代入しようとすると、次のコンパイルエラーになります。

Assigning to 'int (*)(int)' from incompatible type '__bind<int (&)(__sFILE *, long, int), __sFILE &, int, std::__1::placeholders::__ph<1> &>'

エラー 1 error C2440: '=' : 'std::_Bind<_Forced,_Ret,_Fun,_V0_t,_V1_t,_V2_t,_V3_t,_V4_t,_V5_t,<unnamed-symbol>>' から 'int (__cdecl *)(int)' に変換できません。

クロージャーを使ったラムダ関数の場合は次のエラーになりました。

Assigning to 'int (*)(int)' from incompatible type '<lambda at ****.mm:2386:9>'

エラー 1 error C2440: '=' : 'CMyClass::!=::<lambda_05dc6d8d6dcb1b3fa26495af995a3e22>' から 'int (__cdecl *)(int)' に変換できません

このように、いずれも型の不一致がその原因になるようですね。

 

std::function<> を引数に取る関数に C 言語式の関数ポインタを渡す

std::function<> のインスタンスを引数に取る関数を作成した場合、そこに C 言語式の関数ポインタを渡すと、std::function<> の変換コンストラクタのおかげで、自動的にその関数ポインタを内包した std::function<> が作られて渡されます。

void receiver(std::function<int(int)> func);

 

void main()

{

// C 言語式の関数ポインタを std::function<> を引数に取る関数にそのまま渡せます。

receiver(toupper);

}

引数が std::function<> への参照を取る場合でも C 言語式の関数ポインタをそのまま渡せます。

void receiver(const std::function<int(int)>& func);

 

void main()

{

// C 言語式の関数ポインタを std::function<> の参照を引数に取る関数にもそのまま渡せます。

receiver(toupper);

}

std::function<> を引数が受け取る型にすれば、通常の C 言語式の関数ポインタも、ラムダ式も std::bind<> の戻り値も、どれでもそのまま受けられるので便利ですね。

std::function<> の参照を受け取るようにしても問題ないので、支障が無ければ、通常は参照を受け取る指定の方が、呼び出しコストが抑えられて良さそうです。

 

std::function<> を従来の C 言語式の関数ポインタに渡したい場合

std::function<> で受けた関数ポインタを C 言語式の関数ポインタで扱うのは厄介です。

std::function<> には、保持している関数ポインタを取得する target<>() 関数というものがあるのですけど、これはテンプレート関数になっていて、取得時には、取得したい関数の型を明示的に指定する必要があります。

 

それなら、その std::function<> が扱っている関数の型を把握しておけばいいかと言えば、そうでもなくて、単純に戻り値の型と引数の型だけを把握していても、関数ポインタを取得できないケースが出てきます。

 

C 言語式の関数ポインタが代入されている場合

簡単に取得できるのが、std::function<> に C 言語式の関数ポインタが格納されている場合です。

このようなときには、std::function<> の target<> 関数に、テンプレート引数として、格納されているポインタの型を指定することで、その関数ポインタを取得することができます。

p = func.target<int(*)(int)>();

このとき、上記で受けている変数 p の型は、target<> のテンプレート引数に指定した "int(*)(int)" 型と同じです。

 

ここで注意したいのが、target<> 関数に渡すテンプレート引数の書き方が、std::function<> クラスを定義したときとは僅かに違っているところです。

std::function<> を定義したときは "戻り値の型(引数の型,...)" という形の書式でしたけど、target<> 関数でポインタを取得するときには "戻り値の型(*)(引数の型,...)" という表記になるところに注意です。

 

std::function<> が内部で扱っている関数ポインタの型を熟知していることが大前提

また、正確には typeid 演算子を通して、std::function<> が内部で保持している関数ポインタと同じになるものをここで指定します。

内部で保持しているものとは違う型が指定された場合は、別の関数ポインタを持っていても、target<> は nullptr を返します。

たとえ保持している関数ポインタが、戻り値と引数のセットが同じでも、ラムダ関数などの別の形式だった場合は nullptr が取得されることになるので、std::function<> が柔軟に関数ポインタを扱えるのとは裏腹に、それを C 言語の関数ポインタとして取り出すことはなかなか大変です。

 

std::bind<> が代入されている場合

std::bind<> が代入されている場合も、なんとかその関数ポインタを取り出すことが可能です。

C++11 の decltype を使うと、関数の戻り値の型も取得できることを利用して、つまり std::function<> に代入されている型を target<> 関数で指定することができます。

 

もちろん、このときは std::function<> 代入されているのが std::bind<> を使って生成した関数であることが判っていることが大前提になります。

p = func.target<decltype(std::bind(toupper, 'a'))>();

上記で受けている変数 p の型は、target<> のテンプレート引数に指定した std::bind<> の戻り値と同じ型です。

そして target<> 関数のテンプレート引数で指定している decltype の中の "std::bind(toupper, 'a')" のところは、実際にはこれが実行されることはなく、あくまでもどの関数を呼び出せばいいのかを判断するためのヒントとして使われます。

 

このようにすることで、std::function<> に代入されている関数ポインタを取得することはできましたが、このように std::bind<> で代入した場合と、C 言語式の関数ポインタを直接代入した場合とで、テンプレート引数がずいぶん違ってくるのは厄介です。

もちろん、std::function<> が内部で保持している関数ポインタと違う型を指定した場合は nullptr が返されます。

 

代入されているラムダ関数は取得できない?

同じ調子で、代入されているラムダ関数のポインタを取得できればよかったのですけど、今のところどうやってそれを取得したらいいか判りませんでした。

理屈としては、テンプレート引数で指定した型を typeid 演算して、それが std::function<> が内部で保持している関数ポインタの型に一致するものを指定すればいいのですけど、ラムダ関数の場合はそこに何を指定したらいいか分りません。

 

ラムダ関数自体は関数呼び出しではなさそうで decltype は使えませんし、ラムダ関数が返す関数ポインタは内部的には特殊な型になっているようで、それを typeid で正確に言い当てるにはどうしたらいいのでしょうね。

もちろん言い当てられなければ、target<> 関数の戻り値は nullptr になってしまいます。

 

かろうじてできる方法としては、クロージャを含まないラムダ関数に限ってですけど、それは C 言語式の関数ポインタに代入することができるので、ラムダ関数をいったん C 言語方式の関数ポインタに代入してから、そのポインタを std::function<> に代入すれば、通常の C 言語式の関数ポインタを取り出す場合と同じ要領でポインタを取り出すことは可能です。

int(*func1)(int) = [](int a)->int { return a * 2; };

std::function<int(int)> func2 = func1;

 

p = func2.target<int(*)(int)>();

ただ、std::bind<> 関数の場合はこのようなことができないですし、果たしてここまでしてラムダ関数を扱う必要があるのかどうかは疑問です。

 

std::function<> の target<> 関数の使いどころ

このように、std::function<> クラスが持つ target<> 関数では、管理している関数の型を厳重に把握する必要があります。

代入されている関数の戻り値と引数群だけでなく、どのような方法で代入されたかも把握しないといけないため、target<> 関数を使うことで可能性が広がるというよりは、target<> 関数の活用によって制約が増すとも言えそうです。

 

おさらいすると、target<> 関数で取得できるのは、C 言語式の関数ポインタと std::bind<> 関数で生成した関数で、取得できなそうなのはラムダ関数で生成した場合です。

取得できるかのカギは「std::function<> が保持している関数ポインタと typeid が一致する型を target<> 関数のテンプレート引数に指定できるか」です。

 

そんな感じで、std::function<> から関数ポインタを取得して何かに使いたい場合は、関数ポインタの扱いをどれかの方法に統一するとか、プログラミングの方針を予め決めておかないと、とてもじゃないですけど上手くは行かなそうですね。

std::function<> の関数ポインタを取得しなければいけないくらいならいっそ、関数は全て C 言語式の関数ポインタに統一しておいた方が、std::function<> を使うよりも柔軟なプログラミングができるかもしれません。

 

活躍できる場面があるとすれば、たとえば次のようにして、その std::function<> が保持している関数が、C 言語式で存在している関数かどうかを調べるという場面でしょうか。

if (function.target<(int)(*)(int)>() != nullptr)

{

}

ラムダ関数や std::bind<> のような動的に作られた関数の場合は nullptr が返されるので、これによって C 言語式の関数ポインタかどうかを篩い分けすることができます。

動的な関数を対象外としたいような場合があればですけど、そんな時にはちょうどいいかもしれないですね。

 

そんな場面はめったになさそうですけど、たとえば処理が重たい関数がいくつかあったとして、関数の呼び出し結果をキャッシュするような仕組みを持たせたいときとかには、もしかすると良いかもしれません。

普通の関数はキャッシュしたいけど、それらを組み合わせて動的に作った関数もキャッシュすると際限が無いような場合とか、target<> 関数を使って上記のように篩い分ければ、動的な関数を除いた、基本関数だけをひとつのプログラムで効率よくキャッシュする処理を書けたりはしそうです。


[ もどる ]