C++ のファンクターについてよく耳にします。誰かがそれらが何であるか、そしてどのような場合に役立つかについての概要を教えてもらえますか?
14 に答える
ファンクターは、operator()を定義するクラスにすぎません。これにより、関数の「ように見える」オブジェクトを作成できます。
// this is a functor
struct add_x {
add_x(int val) : x(val) {} // Constructor
int operator()(int y) const { return x + y; }
private:
int x;
};
// Now you can use it like this:
add_x add42(42); // create an instance of the functor class
int i = add42(8); // and "call" it
assert(i == 50); // and it added 42 to its argument
std::vector<int> in; // assume this contains a bunch of values)
std::vector<int> out(in.size());
// Pass a functor to std::transform, which calls the functor on every element
// in the input sequence, and stores the result to the output sequence
std::transform(in.begin(), in.end(), out.begin(), add_x(1));
assert(out[i] == in[i] + 1); // for all i
ファンクターにはいくつかの良い点があります。1つは、通常の関数とは異なり、状態を含めることができるということです。上記の例では、指定したものに42を追加する関数を作成します。ただし、その値42はハードコーディングされておらず、ファンクターインスタンスを作成したときにコンストラクター引数として指定されました。別の値でコンストラクターを呼び出すだけで、27を追加する別の加算器を作成できます。これにより、それらを適切にカスタマイズできます。
最後の行が示すように、std::transformや他の標準ライブラリアルゴリズムなどの他の関数への引数としてファンクターを渡すことがよくあります。通常の関数ポインターでも同じことができますが、前述のように、ファンクターは状態が含まれているため「カスタマイズ」でき、柔軟性が高くなります(関数ポインターを使用する場合は、関数を作成する必要があります)。これは引数に正確に1を追加しました。ファンクターは一般的であり、初期化したものは何でも追加します)、またそれらは潜在的により効率的です。上記の例では、コンパイラはどの関数std::transform
を呼び出すべきかを正確に認識しています。を呼び出す必要がありますadd_x::operator()
。つまり、その関数呼び出しをインライン化できるということです。これにより、ベクトルの各値に対して手動で関数を呼び出した場合と同じように効率的になります。
代わりに関数ポインターを渡した場合、コンパイラーはそれが指す関数をすぐに確認できなかったため、かなり複雑なグローバル最適化を実行しない限り、実行時にポインターを逆参照してから呼び出しを行う必要があります。
Little addition. You can use boost::function
, to create functors from functions and methods, like this:
class Foo
{
public:
void operator () (int i) { printf("Foo %d", i); }
};
void Bar(int i) { printf("Bar %d", i); }
Foo foo;
boost::function<void (int)> f(foo);//wrap functor
f(1);//prints "Foo 1"
boost::function<void (int)> b(&Bar);//wrap normal function
b(1);//prints "Bar 1"
and you can use boost::bind to add state to this functor
boost::function<void ()> f1 = boost::bind(foo, 2);
f1();//no more argument, function argument stored in f1
//and this print "Foo 2" (:
//and normal function
boost::function<void ()> b1 = boost::bind(&Bar, 2);
b1();// print "Bar 2"
and most useful, with boost::bind and boost::function you can create functor from class method, actually this is a delegate:
class SomeClass
{
std::string state_;
public:
SomeClass(const char* s) : state_(s) {}
void method( std::string param )
{
std::cout << state_ << param << std::endl;
}
};
SomeClass *inst = new SomeClass("Hi, i am ");
boost::function< void (std::string) > callback;
callback = boost::bind(&SomeClass::method, inst, _1);//create delegate
//_1 is a placeholder it holds plase for parameter
callback("useless");//prints "Hi, i am useless"
You can create list or vector of functors
std::list< boost::function<void (EventArg e)> > events;
//add some events
....
//call them
std::for_each(
events.begin(), events.end(),
boost::bind( boost::apply<void>(), _1, e));
There is one problem with all this stuff, compiler error messages is not human readable :)
ファンクターは、関数のように機能するオブジェクトです。基本的に、を定義するクラスoperator()
。
class MyFunctor
{
public:
int operator()(int x) { return x * 2;}
}
MyFunctor doubler;
int x = doubler(5);
本当の利点は、ファンクターが状態を保持できることです。
class Matcher
{
int target;
public:
Matcher(int m) : target(m) {}
bool operator()(int x) { return x == target;}
}
Matcher Is5(5);
if (Is5(n)) // same as if (n == 5)
{ ....}
「ファンクター」という名前は、C++ が登場するずっと前から、伝統的に圏論で使用されてきました。これは、C++ のファンクターの概念とは関係ありません。C++ で「ファンクター」と呼ぶものではなく、名前の関数オブジェクトを使用することをお勧めします。これは、他のプログラミング言語が同様の構造を呼び出す方法です。
単純な関数の代わりに使用:
特徴:
- 関数オブジェクトは状態を持つことができます
- 関数オブジェクトは OOP に適合します (他のすべてのオブジェクトと同じように動作します)。
短所:
- プログラムをより複雑にします。
関数ポインタの代わりに使用:
特徴:
- 関数オブジェクトはしばしばインライン化されることがあります
短所:
- 実行時に関数オブジェクトを他の関数オブジェクト型と交換することはできません (少なくとも、基本クラスを拡張しない限り、オーバーヘッドが発生します)。
仮想関数の代わりに使用:
特徴:
- 関数オブジェクト (非仮想) は、vtable とランタイムのディスパッチを必要としないため、ほとんどの場合、より効率的です。
短所:
- 実行時に関数オブジェクトを他の関数オブジェクト型と交換することはできません (少なくとも、基本クラスを拡張しない限り、オーバーヘッドが発生します)。
他の人が述べたように、ファンクターは関数のように機能するオブジェクトです。つまり、関数呼び出し演算子をオーバーロードします。
ファンクターは、STL アルゴリズムで一般的に使用されます。関数型言語のクロージャーのように、関数呼び出しの前と呼び出しの間で状態を保持できるため、便利です。たとえばMultiplyBy
、指定した量で引数を乗算するファンクターを定義できます。
class MultiplyBy {
private:
int factor;
public:
MultiplyBy(int x) : factor(x) {
}
int operator () (int other) const {
return factor * other;
}
};
MultiplyBy
次に、std::transform のようなアルゴリズムにオブジェクトを渡すことができます。
int array[5] = {1, 2, 3, 4, 5};
std::transform(array, array + 5, array, MultiplyBy(3));
// Now, array is {3, 6, 9, 12, 15}
関数へのポインターに対するファンクターのもう 1 つの利点は、より多くの場合に呼び出しをインライン化できることです。関数ポインターを に渡した場合、その呼び出しがインライン化され、常に同じ関数を渡すことをコンパイラーが認識しない限り、ポインターを介して呼び出しをインライン化することはできませんtransform
。
私たちの中の私のような初心者のために:少し調査した後、jalfが投稿したコードが何をするかを理解しました.
ファンクターは、関数のように「呼び出す」ことができるクラスまたは構造体オブジェクトです。これは、 をオーバーロードすることで可能になり() operator
ます。(() operator
何と呼ばれるかは不明)は、任意の数の引数を取ることができます。他の演算子は 2 つしか取りません。つまり、+ operator
は 2 つの値 (演算子の両側に 1 つずつ) のみを取り、オーバーロードした値を返すことができます。任意の数の引数を a 内に収めることができるため() operator
、柔軟性が得られます。
ファンクターを作成するには、まずクラスを作成します。次に、選択したタイプと名前のパラメーターを使用して、クラスのコンストラクターを作成します。これに続いて、同じステートメントで初期化子リスト (単一のコロン演算子を使用します。これは私も初めて知りました) で、以前にコンストラクターに宣言されたパラメーターを使用してクラス メンバー オブジェクトを構築します。次に、() operator
が過負荷になります。最後に、作成したクラスまたは構造体のプライベート オブジェクトを宣言します。
私のコード (jalf の変数名が紛らわしいことがわかりました)
class myFunctor
{
public:
/* myFunctor is the constructor. parameterVar is the parameter passed to
the constructor. : is the initializer list operator. myObject is the
private member object of the myFunctor class. parameterVar is passed
to the () operator which takes it and adds it to myObject in the
overloaded () operator function. */
myFunctor (int parameterVar) : myObject( parameterVar ) {}
/* the "operator" word is a keyword which indicates this function is an
overloaded operator function. The () following this just tells the
compiler that () is the operator being overloaded. Following that is
the parameter for the overloaded operator. This parameter is actually
the argument "parameterVar" passed by the constructor we just wrote.
The last part of this statement is the overloaded operators body
which adds the parameter passed to the member object. */
int operator() (int myArgument) { return myObject + myArgument; }
private:
int myObject; //Our private member object.
};
これのいずれかが不正確であるか、単に間違っている場合は、お気軽に修正してください!
ファンクターは、パラメーター化された (つまり、テンプレート化された) 型に関数を適用する高階関数です。写像高階関数の一般化です。たとえば、次のstd::vector
ようにファンクターを定義できます。
template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::vector<U> fmap(F f, const std::vector<T>& vec)
{
std::vector<U> result;
std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);
return result;
}
この関数は、 を取り、std::vector<T>
を返すstd::vector<U>
関数が与えられると、F
を取り、T
を返しますU
。ファンクタは、コンテナ型に対して定義する必要はありません。次のようなテンプレート化された型に対しても定義できますstd::shared_ptr
。
template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>
std::shared_ptr<U> fmap(F f, const std::shared_ptr<T>& p)
{
if (p == nullptr) return nullptr;
else return std::shared_ptr<U>(new U(f(*p)));
}
タイプを に変換する簡単な例を次に示しますdouble
。
double to_double(int x)
{
return x;
}
std::shared_ptr<int> i(new int(3));
std::shared_ptr<double> d = fmap(to_double, i);
std::vector<int> is = { 1, 2, 3 };
std::vector<double> ds = fmap(to_double, is);
ファンクタが従うべき法則が 2 つあります。1 つ目は恒等法則です。ファンクタに恒等関数が与えられている場合、それは型に恒等関数を適用するのと同じでなければならない、つまり とfmap(identity, x)
同じでなければならない、と述べていidentity(x)
ます。
struct identity_f
{
template<class T>
T operator()(T x) const
{
return x;
}
};
identity_f identity = {};
std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<int> is1 = fmap(identity, is);
std::vector<int> is2 = identity(is);
次の法則は合成法則であり、ファンクターに 2 つの関数の合成が与えられた場合、最初の関数にファンクターを適用し、次に 2 番目の関数に適用するのと同じであると述べています。したがって、次fmap(std::bind(f, std::bind(g, _1)), x)
と同じである必要がありますfmap(f, fmap(g, x))
。
double to_double(int x)
{
return x;
}
struct foo
{
double x;
};
foo to_foo(double x)
{
foo r;
r.x = x;
return r;
}
std::vector<int> is = { 1, 2, 3 };
// These two statements should be equivalent.
// is1 should equal is2
std::vector<foo> is1 = fmap(std::bind(to_foo, std::bind(to_double, _1)), is);
std::vector<foo> is2 = fmap(to_foo, fmap(to_double, is));
問題を解決するために Functor を使用せざるを得なくなった実際の状況を次に示します。
関数のセット (たとえば、20 個) があり、それぞれが 3 つの特定の場所で異なる特定の関数を呼び出すことを除いて、それらはすべて同一です。
これは信じられないほどの無駄であり、コードの重複です。通常、私は関数ポインタを渡し、それを 3 つの場所で呼び出すだけです。(したがって、コードは 20 回ではなく、1 回だけ表示する必要があります。)
しかし、いずれの場合も、特定の機能にはまったく異なるパラメーター プロファイルが必要であることに気付きました。2 つのパラメーターの場合もあれば、5 つのパラメーターの場合もあります。
別の解決策は、特定の関数が派生クラスのオーバーライドされたメソッドである基本クラスを持つことです。しかし、関数ポインタを渡すためだけに、この INHERITANCE をすべて構築する必要があるのでしょうか????
解決策:私がしたことは、呼び出す必要のある関数を呼び出すことができるラッパークラス(「ファンクター」)を作成することでした。事前に (パラメーターなどを使用して) 設定し、関数ポインターの代わりに渡します。呼び出されたコードは、内部で何が起こっているかを知らなくても Functor をトリガーできるようになりました。複数回呼び出すこともできます(3回呼び出す必要がありました)。
以上です - Functor が明白で簡単なソリューションであることが判明した実用的な例です。これにより、コードの重複を 20 個の関数から 1 個に減らすことができました。
繰り返しになりますが、ファンクターは関数(オーバーロード演算子())として扱えるクラスです。
これらは、一部のデータを関数の繰り返し呼び出しまたは遅延呼び出しに関連付ける必要がある状況で最も役立ちます。
たとえば、ファンクターのリンク リストを使用して、基本的な低オーバーヘッドの同期コルーチン システム、タスク ディスパッチャー、または割り込み可能なファイル解析を実装できます。例:
/* prints "this is a very simple and poorly used task queue" */
class Functor
{
public:
std::string output;
Functor(const std::string& out): output(out){}
operator()() const
{
std::cout << output << " ";
}
};
int main(int argc, char **argv)
{
std::list<Functor> taskQueue;
taskQueue.push_back(Functor("this"));
taskQueue.push_back(Functor("is a"));
taskQueue.push_back(Functor("very simple"));
taskQueue.push_back(Functor("and poorly used"));
taskQueue.push_back(Functor("task queue"));
for(std::list<Functor>::iterator it = taskQueue.begin();
it != taskQueue.end(); ++it)
{
*it();
}
return 0;
}
/* prints the value stored in "i", then asks you if you want to increment it */
int i;
bool should_increment;
int doSomeWork()
{
std::cout << "i = " << i << std::endl;
std::cout << "increment? (enter the number 1 to increment, 0 otherwise" << std::endl;
std::cin >> should_increment;
return 2;
}
void doSensitiveWork()
{
++i;
should_increment = false;
}
class BaseCoroutine
{
public:
BaseCoroutine(int stat): status(stat), waiting(false){}
void operator()(){ status = perform(); }
int getStatus() const { return status; }
protected:
int status;
bool waiting;
virtual int perform() = 0;
bool await_status(BaseCoroutine& other, int stat, int change)
{
if(!waiting)
{
waiting = true;
}
if(other.getStatus() == stat)
{
status = change;
waiting = false;
}
return !waiting;
}
}
class MyCoroutine1: public BaseCoroutine
{
public:
MyCoroutine1(BaseCoroutine& other): BaseCoroutine(1), partner(other){}
protected:
BaseCoroutine& partner;
virtual int perform()
{
if(getStatus() == 1)
return doSomeWork();
if(getStatus() == 2)
{
if(await_status(partner, 1))
return 1;
else if(i == 100)
return 0;
else
return 2;
}
}
};
class MyCoroutine2: public BaseCoroutine
{
public:
MyCoroutine2(bool& work_signal): BaseCoroutine(1), ready(work_signal) {}
protected:
bool& work_signal;
virtual int perform()
{
if(i == 100)
return 0;
if(work_signal)
{
doSensitiveWork();
return 2;
}
return 1;
}
};
int main()
{
std::list<BaseCoroutine* > coroutineList;
MyCoroutine2 *incrementer = new MyCoroutine2(should_increment);
MyCoroutine1 *printer = new MyCoroutine1(incrementer);
while(coroutineList.size())
{
for(std::list<BaseCoroutine *>::iterator it = coroutineList.begin();
it != coroutineList.end(); ++it)
{
*it();
if(*it.getStatus() == 0)
{
coroutineList.erase(it);
}
}
}
delete printer;
delete incrementer;
return 0;
}
もちろん、これらの例自体はあまり役に立ちません。それらはファンクターがどのように役立つかを示すだけであり、ファンクター自体は非常に基本的で柔軟性がなく、たとえばブーストが提供するものよりも有用性が低くなります。
コールバックで使用される場合を除き、C++ ファンクターは、Matlab好みのアクセス スタイルを行列 クラスに提供するのにも役立ちます。例あり。
ファンクターは gtkmm で使用され、いくつかの GUI ボタンを実際の C++ 関数またはメソッドに接続します。
pthread ライブラリを使用してアプリをマルチスレッド化する場合、Functor が役立ちます。
スレッドを開始するための引数の 1 つは、pthread_create(..)
自分のスレッドで実行される関数ポインタです。
ただ、ひとつ不便な点があります。このポインターは、静的メソッドでない限り、またはのようにクラスを指定しない限り、メソッドへのポインターにすることはできませんclass::method
。もう1つ、メソッドのインターフェースは次のとおりです。
void* method(void* something)
したがって、何か特別なことをしなければ、スレッド内のクラスのメソッドを (単純な明白な方法で) 実行することはできません。
C++ でスレッドを処理する非常に優れた方法は、独自のThread
クラスを作成することです。クラスからメソッドを実行したい場合、私がしたことは、それらのメソッドを派生クラス MyClass
に変換することでした。Functor
また、このThread
クラスには次
のメソッドがあります。static void* startThread(void* arg)
このメソッドへのポインタは、 call の引数として使用されますpthread_create(..)
。そしてstartThread(..)
、引数で受け取る必要があるのは、派生クラスvoid*
のヒープ内のインスタンスへのキャストされた参照です。これは、実行時にキャストされ、そのメソッドが呼び出されます。Functor
Functor*
run()
さらに、関数オブジェクトを使用して、既存のレガシー メソッドをコマンド パターンに適合させました。(OO パラダイムの真の OCP の美しさを感じた場所だけ); また、関連する関数アダプター パターンをここに追加します。
メソッドに署名があるとします。
int CTask::ThreeParameterTask(int par1, int par2, int par3)
Command パターンにどのように適合させるかを見ていきます。そのためには、まず、関数オブジェクトとして呼び出せるようにメンバー関数アダプターを作成する必要があります。
注 - これは見苦しく、Boost バインド ヘルパーなどを使用できる可能性がありますが、使用できない場合、または使用したくない場合は、これが 1 つの方法です。
// a template class for converting a member function of the type int function(int,int,int)
//to be called as a function object
template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
class mem_fun3_t
{
public:
explicit mem_fun3_t(_Ret (_Class::*_Pm)(_arg1,_arg2,_arg3))
:m_Ptr(_Pm) //okay here we store the member function pointer for later use
{}
//this operator call comes from the bind method
_Ret operator()(_Class *_P, _arg1 arg1, _arg2 arg2, _arg3 arg3) const
{
return ((_P->*m_Ptr)(arg1,arg2,arg3));
}
private:
_Ret (_Class::*m_Ptr)(_arg1,_arg2,_arg3);// method pointer signature
};
mem_fun3
また、呼び出しを支援するために、上記のクラスのヘルパー メソッドが必要です。
template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3> mem_fun3 ( _Ret (_Class::*_Pm) (_arg1,_arg2,_arg3) )
{
return (mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3>(_Pm));
}
ここで、パラメーターをバインドするために、バインダー関数を作成する必要があります。だから、ここに行きます:
template<typename _Func,typename _Ptr,typename _arg1,typename _arg2,typename _arg3>
class binder3
{
public:
//This is the constructor that does the binding part
binder3(_Func fn,_Ptr ptr,_arg1 i,_arg2 j,_arg3 k)
:m_ptr(ptr),m_fn(fn),m1(i),m2(j),m3(k){}
//and this is the function object
void operator()() const
{
m_fn(m_ptr,m1,m2,m3);//that calls the operator
}
private:
_Ptr m_ptr;
_Func m_fn;
_arg1 m1; _arg2 m2; _arg3 m3;
};
そして、binder3 クラスを使用するヘルパー関数 - bind3
:
//a helper function to call binder3
template <typename _Func, typename _P1,typename _arg1,typename _arg2,typename _arg3>
binder3<_Func, _P1, _arg1, _arg2, _arg3> bind3(_Func func, _P1 p1,_arg1 i,_arg2 j,_arg3 k)
{
return binder3<_Func, _P1, _arg1, _arg2, _arg3> (func, p1,i,j,k);
}
これを Command クラスで使用する必要があります。次の typedef を使用します。
typedef binder3<mem_fun3_t<int,T,int,int,int> ,T* ,int,int,int> F3;
//and change the signature of the ctor
//just to illustrate the usage with a method signature taking more than one parameter
explicit Command(T* pObj,F3* p_method,long timeout,const char* key,
long priority = PRIO_NORMAL ):
m_objptr(pObj),m_timeout(timeout),m_key(key),m_value(priority),method1(0),method0(0),
method(0)
{
method3 = p_method;
}
呼び方は次のとおりです。
F3 f3 = PluginThreadPool::bind3( PluginThreadPool::mem_fun3(
&CTask::ThreeParameterTask), task1,2122,23 );
注:f3();
はメソッドを呼び出しますtask1->ThreeParameterTask(21,22,23);
。
次のリンクにあるこのパターンの完全なコンテキスト
関数をファンクターとして実装する大きな利点は、呼び出し間で状態を維持および再利用できることです。たとえば、文字列間のレーベンシュタイン距離を計算するWagner-Fischer アルゴリズムなど、多くの動的プログラミング アルゴリズムは、結果の大きなテーブルを埋めることによって機能します。関数が呼び出されるたびにこのテーブルを割り当てるのは非常に非効率的であるため、関数をファンクターとして実装し、テーブルをメンバー変数にすることで、パフォーマンスを大幅に向上させることができます。
以下は、Wagner-Fischer アルゴリズムをファンクターとして実装する例です。operator()
テーブルがコンストラクターでどのように割り当てられ、必要に応じてサイズを変更して で再利用されるかに注目してください。
#include <string>
#include <vector>
#include <algorithm>
template <typename T>
T min3(const T& a, const T& b, const T& c)
{
return std::min(std::min(a, b), c);
}
class levenshtein_distance
{
mutable std::vector<std::vector<unsigned int> > matrix_;
public:
explicit levenshtein_distance(size_t initial_size = 8)
: matrix_(initial_size, std::vector<unsigned int>(initial_size))
{
}
unsigned int operator()(const std::string& s, const std::string& t) const
{
const size_t m = s.size();
const size_t n = t.size();
// The distance between a string and the empty string is the string's length
if (m == 0) {
return n;
}
if (n == 0) {
return m;
}
// Size the matrix as necessary
if (matrix_.size() < m + 1) {
matrix_.resize(m + 1, matrix_[0]);
}
if (matrix_[0].size() < n + 1) {
for (auto& mat : matrix_) {
mat.resize(n + 1);
}
}
// The top row and left column are prefixes that can be reached by
// insertions and deletions alone
unsigned int i, j;
for (i = 1; i <= m; ++i) {
matrix_[i][0] = i;
}
for (j = 1; j <= n; ++j) {
matrix_[0][j] = j;
}
// Fill in the rest of the matrix
for (j = 1; j <= n; ++j) {
for (i = 1; i <= m; ++i) {
unsigned int substitution_cost = s[i - 1] == t[j - 1] ? 0 : 1;
matrix_[i][j] =
min3(matrix_[i - 1][j] + 1, // Deletion
matrix_[i][j - 1] + 1, // Insertion
matrix_[i - 1][j - 1] + substitution_cost); // Substitution
}
}
return matrix_[m][n];
}
};