Lambda関数とC ++での便利なコールバックメカニズムの実装

この記事では、例としてコールバックメカニズムを使用して、便利で素早い形式でラムダ関数を使用する可能性を検討します。



問題の声明



任意の関数への「ポインタ」を保存し、引数で呼び出すための便利で高速なメカニズムを実装する必要があります(たとえば、char *型を使用します)。



方法1-古典的な「C」



「額」の問題を解決すると、次のようなものが得られます。



//   static void MyFunction(char *s){ puts(s); } int main(){ //,     void (*MyCallback)(char *argument); //     MyCallback=MyFunction; //    MyCallback("123"); return 0; }
      
      





このメカニズムは非常にシンプルで、よく使用されます。 しかし、多数のコールバックがあると、それらのアナウンスはあまり便利ではなくなります。



C ++のLambda関数



C ++ 11(またはC ++ 0x)について聞いたことがない、またはまだ触れていない人のために、この標準のいくつかの革新についてお話します。 C ++ 11では、初期化で変数を宣言するときに型の代わりに設定できるautoキーワードが登場しました。 この場合、変数の型は「=」の後に表示される型と同じになります。 例:

  auto a=1; //    int a=1; auto b=""; //    const char* b=1; auto c=1.2; //    double c=1; auto d; // !     d
      
      







しかし、最も興味深いのはラムダ関数です。 原則として、これらは通常の関数ですが、式で直接宣言できます。

 [](int a,int b) -> bool //    ,  bool { return a>b; }
      
      







ラムダ関数の構文は次のとおりです。



 [ ]()-> {   }
      
      







「-> return type」ピースが欠落している可能性があります。 次に、「-> void」を意味します。 別の使用例:

 int main(int argc,char *argv[]){ //,  abs(int) auto f1=[](int a)->int{ return (a>0)?(a):(-a); }; //,     0.0  1.0 auto f2=[]()->float{ return float(rand())/RAND_MAX; }; //,   enter auto f3=[](){ puts("Press enter to continue..."); getchar(); }; printf("%d %d\n",f1(5),f1(-10)); printf("%f %f\n",f2(),f2()); f3(); return 0; }
      
      







このプログラムは次を出力します。



 5 10 0.563585 0.001251 Press enter to continue...
      
      







この例では、auto型の3つの変数(f1、f2、f3)が宣言および初期化されているため、その型は右側の型(ラムダ関数の型)に対応しています。

ラムダ関数は、それ自体では関数へのポインターではありません(ただし、場合によっては関数に移動できます)。 コンパイラはアドレスで関数を呼び出しませんが、その型に応じて-各関数lambdaが独自の型、たとえば「<lambda_a48784a181f11f18d942adab3de2ffca>」を持つ理由です。 このタイプは指定できないため、autoまたはテンプレートと組み合わせてのみ使用できます(タイプはそこで自動的に決定することもできます)。

標準では、キャプチャされた変数がない場合、ラムダ型から関数ポインター型への変換も許可されています。



 void(*func)(int arg); func= [](int arg){ ... }; //  ,  
      
      







キャプチャされた変数は、ラムダ関数が指定されたときにラムダ関数の「内部に入る」変数です。

 int main(int argc,char *argv[]){ auto f=[argc,&argv](char *s){ puts(s); for(int c=0;c<argc;c++){ puts(argv[c]); } }; f("123"); return 0; }
      
      







これらのパラメーターは、実際には変数fに保存されます(値によってコピーされます)。

名前の前に&記号を指定すると、パラメーターは値ではなく参照によって渡されます。

関数自体のアドレスはまだどこにも保存されていません。



方法2-C ++での実装



静的関数をラムダに置き換えると、例を簡単にできます。



 int main(){ void (*MyCallback)(char *argument); //      ! MyCallback=[](char *s){ puts(s); }; MyCallback("123"); return 0; }
      
      







したがって、少しの「プラス」を追加することで人生を大幅に簡素化できます。主なことは、やり過ぎないことです。 この例では、このような構造は、ラムダ関数の変数を「キャプチャ」するまで機能します。 そうすると、コンパイラはラムダをポインターに変換できなくなります。 ここでは、C ++を使用してこれを行うことができます。



 class Callback{ private: // ,       class FuncClass{ public: //   virtual void Call(char*)=0; }; //     FuncClass *function; public: Callback(){ function=0; } ~Callback(){ if(function) delete function; } template<class T> void operator=(T func){ if(function) delete function; //     Call,  func class NewFuncClass:public FuncClass{ public: T func; NewFuncClass(T f):func(f){ } void Call(char* d){ func(d); } }; //       function=new NewFuncClass(func); } void operator()(char* d){ if(function) function->Call(d); } }; int main(){ Callback MyCallback; MyCallback=[](char *s){ puts(s); }; MyCallback("123"); return 0; }
      
      







行くぞ わずかにプラスになり、コードは数倍になります。 面倒な実装であり、Callbackインスタンスをコピーする可能性はまだ考慮されていません。 しかし、使いやすさが一番です。 また、控えめな操作「=」は、動的メモリの割り当て、さらにはコンストラクターを隠します。これは、古典的な「C」プログラマーに愛用されているコードの視覚化の概念に明らかに適合しません。



利便性を損なうことなく、可能な限り修正して、実装をできるだけ高速化および簡素化してみましょう。



方法3-中間の何か



実装:

 class Callback{ private: void (*function)(char*,void*); void *parameters[4]; public: Callback(){ function=[](char*,void*){ }; } template<class T> void operator=(T func){ //    ,  sizeof(T) <= sizeof(parameters) //    ,   compile-time , .. //      sizeof(int[ sizeof(parameters)-sizeof(T) ]); //    ,     func function=[](char* arg,void *param){ (*(T*)param)(arg); }; //     func  parameters memcpy(parameters,&func,sizeof(T)); } void operator()(char* d){ //     function,    parameters function(d,parameters); } }; int main(){ Callback MyCallback; MyCallback=[](char *s){ puts(s); }; MyCallback("123"); return 0; }
      
      







まず、仮想機能とメモリ割り当てに関連する大きなチャンクを削除しました。 保存は、数バイトのコピー速度で行われます。



呼び出しも迅速です-コンパイラが一方をもう一方に埋め込むときに、2つのネストされた関数(補助および保存)を呼び出します-ほぼ完璧なオプション(1つの追加引数「パラメータ」が理想を分離します)。



そのような実装の場合、唯一の制限は、ラムダ関数でキャプチャされる変数の最大サイズです。 しかし、通常、それほど多くの追加パラメーターを渡す必要はありません。 また、多数の場合、速度を犠牲にして動的メモリを使用できます。



まとめ



ポインターとしての関数転送の利便性と機能性は、リソース消費を大幅に増加させることなく、高度な利便性をもたらしました。 機能に関しては、創造性の余地がまだ十分にあります。優先度(イベントフロー)を含むキューの作成、さまざまな種類の引数のテンプレートなどです。



All Articles