最近C#を勉強しようと思い、覚えることというよりは機能の多さに目眩がしてしまってC++に逃げる毎日が続いています。C#は簡単に色々なことができますが、反面少し深く腕を突っ込んでみると途端に情報量の多さに腕を持って行かれてしまいます。
さて、C#にはデリゲートという機能があり、これは最も単純に言えば同じシグニチャを持つ関数を登録すると一度に実行できるというものです。
簡単に言えば、関数ポインタの配列を作って、関数を登録して、一気に実行する、というイメージですね。
これをC++でもと思い作ってみました。
(恐らく誰でも一度は考えそうなことなので車輪の再発明もいいところだとは思いますが・・)
黒魔術とは言いませんが、やってることの割には複雑そうに見えます。
しかし実際はstd::functionのラッパに過ぎません。
// ヘルパ関数
template < class Ty >
void* union_cast( Ty ptr )
{ // メンバ関数ポインタをvoid*に変換する
// reinterpret_castでもできない危険なキャスト
union
{
Ty _mem_func_ptr;
void* _void_ptr;
};
_mem_func_ptr = ptr;
return _void_ptr;
}
//
// デリゲートクラス
template < class >
class delegate {};
template < class Ret, class... Args >
class delegate < Ret( Args... ) >
{
using _KeyPair = std::pair < void*, void* >;
struct _Value
{
std::function < Ret( Args... ) > function;
_KeyPair keys;
};
std::vector < _Value > value;
auto find( void* first, void* second = nullptr ) NOEXCEPT
{
_KeyPair arg { first, second };
return std::find_if( value.begin(), value.end(), [&]( _Value& val )
{ // lambda
return val.keys == arg;
} );
}
public:
//
// add function
//
template < class Ty >
void add( Ty* func ) // 関数ポインタ用
{ // function-pointer
if( find( func ) != value.end() ) { return; }
value.push_back( { [=]( Args... args ) -> Ret
{ // lambda
return func( std::forward < Args >( args )... );
}, _KeyPair( func, nullptr ) } );
}
template < class Ty >
void add( Ty& func ) // 関数オブジェクト用
{ // function-object
if( find( &func ) != value.end() ) { return; }
value.push_back( { [&]( Args... args ) -> Ret
{ // lambda
return func( std::forward < Args >( args )... );
}, _KeyPair( &func, nullptr ) } );
}
template < class Ty >
void add( Ty&& func ) // 関数オブジェクト用
{ // function-object
value.push_back( { [func = std::move( func )] ( Args... args ) mutable -> Ret
{ // lambda
return func( std::forward < Args >( args )... );
}, _KeyPair( nullptr, nullptr ) } );
}
template < class Ty1, class Ty2 >
void add( Ty1& obj, Ty2 ptr ) // メンバ関数ポインタ用
{ // member-function
if( find( &obj, union_cast( ptr ) ) != value.end() ) { return; }
value.push_back( { [&obj, ptr]( Args... args ) -> Ret
{ // lambda
return ( obj.*ptr )( std::forward < Args >( args )... );
}, _KeyPair( &obj, union_cast( ptr ) ) } );
}
//
// remove function
//
template < class Ty >
void remove( Ty* func ) // 関数ポインタ用
{
auto itr = find( func );
if( itr != value.end() ) { value.erase( itr ); }
}
template < class Ty >
void remove( Ty& func ) // 関数オブジェクト用
{
remove( &func );
}
template < class Ty1, class Ty2 >
void remove( Ty1& obj, Ty2 ptr ) // メンバ関数ポインタ用
{
auto itr = find( &obj, union_cast( ptr ) );
if( itr != value.end() ) { value.erase( itr ); }
}
//
// invoke functions
//
void operator() ( Args... args ) const
{
for( auto&& elem : value ) { elem.function( std::forward < Args >( args )... ); }
}
};
関数ポインタ、関数オブジェクト、メンバ関数用、それぞれのaddとremove関数があります。
一番核になる機能だけ載せてありますが、ほんとはちゃんとしたメンバ関数をもたせるといいですね。swap, reserve, etc...
void func();
struct func_t { void operator()(); };
struct object { void func(); };
int main()
{
delegate < void() > funcs;
func_t func_obj;
object obj;
// 登録
funcs.add( &func ); // 通常の関数は関数ポインタ
funcs.add( func_obj ); // 関数オブジェクトは参照
funcs.add( obj, &object::func ); // this参照とメンバ関数ポインタ
funcs.add( []() { std::cout << "right lambda" << std::endl; } ); // ラムダ式(右辺値)
auto lambda = []() { std::cout << "left lambda" << std::endl; };
funcs.add( lambda ); // ラムダ式(左辺値)
funcs(); // invoke all functions
// 削除
funcs.remove( &func );
funcs.remove( func_obj );
funcs.remove( obj, &object::func );
funcs.remove( lambda );
// 右辺値で追加した関数は削除できません
funcs(); // output "left lambda"
}
できればadd
やremove
じゃなくて+=
や-=
を使いたかったのですが、引数の関係でおじゃんに…
メンバ関数ポインタとthisの仲を裂くようなことはできませんね。
ちなみにこのデリゲートクラスは参照を保持するため、関数オブジェクトやメンバ関数を登録した場合副作用がある場合があります。また、寿命が終了したオブジェクトを保持させると実行時に未定義動作を引き起こします。
実際に使うなら、addやremove関数はメタプログラミングを使って安全性を高めたり、operator()で戻り値のvectorを返してあげるなどするとよさそうです。戻り値を作成する場合は、Retがvoid型であるか否かでtag-dispatchingをしてあげるのがよいでしょう。
追記. 右辺値を受け取るadd関数を追加しました。
追記2. びっくりするくらい似たタイトルの記事を見つけました。中身は全然違いますが…
=> [C++] C++でC#っぽいdelegateを実装してみる(任意クラスの任意メソッド実行)
この記事を見ると、メンバ関数の登録はユーザにラムダ式等に包めてもらう形にして、+=
や-=
演算子を使うようにすればよかったかなあとも思います。