C++の関数オブジェクト(Function Object)(一)

5457 ワード

括弧演算子()が再ロードされた関数オブジェクトです.この演算子を呼び出すと、一般的な関数呼び出しのように表現されるので、関数オブジェクトと名付けられます.最も簡単な例を挙げます.
class FuncObjType
{
public:
	void operator() ()
	{
		cout<

クラスFuncObjTypeに「()」オペレータがリロードされているため、クラスのオブジェクトFuncObjType valに対して、このようにオペレータ:val()を呼び出すことができます.呼び出し結果は、上記のコードの内容を出力します.この呼び出し文は、次の関数の呼び出しと同じ形式で使用されます.
void val()
{
	cout<

関数オブジェクトを呼び出すのと普通の関数を呼び出すのと同じ効果がある以上、なぜ関数オブジェクトを使用するためにクラスを定義するのが面倒なのでしょうか.主に、関数オブジェクトには次のような利点があります.
1.関数オブジェクトは、自分の状態を持つことができます.クラスでステータス変数を定義できます.このような関数オブジェクトは、複数回の呼び出しでこのステータスを共有できます.しかし、グローバル変数を使用してステータスを保存しない限り、関数呼び出しにはこのような利点はありません.ええと、オブジェクト向けにプログラミングします...グローバル変数...
2.関数オブジェクトには独自のタイプがあり、通常の関数にはタイプがありません.この特性はC++標準ライブラリの使用にとって極めて重要である.これにより、STLの関数を使用するときに、対応するタイプをパラメータとして渡して、対応するテンプレートをインスタンス化し、独自に定義したルールを実現することができます.たとえば、カスタムコンテナのソートルールです.
関数オブジェクトの使用例をいくつか示します.
1つ目は、カスタムコンテナsetによる文字列stringのソートルールです.まず、対応するクラスを定義し、「()」を再ロードしてソートを規定します(ここでは、大きいから小さいまでソートします).
class StringSort
{
public:
	bool operator() (const string &str1, const string &str2) const
	{
		return str1 > str2;
	}
};

次に、このクラスをパラメータとしてsetコンテナを初期化できます.
set myset;
myset.insert("A");
myset.insert("B");

このように容器の内容は、デフォルトの「A」,「B」ではなく「A」と出力される.
内部状態に実用的な関数オブジェクトの例をもう1つ挙げます.1つの関数オブジェクトによって連続した数字を生成することを想定し,任意に開始数字を規定することができ,呼び出すたびに次の数字を返す.対応するクラスは次のように設計できます.
class SuccessiveNumGen
{
public:
	SuccessiveNumGen(int origin = 0):m_origin(origin){}
	int operator() ()
	{
		return m_origin++;
	}
private:
	int m_origin;
};

テストのために、標準ライブラリ関数:generate_を使用します.n.この関数は、私たちが転送した関数オブジェクトをn回自動的に呼び出し、指定した開始反復器に結果を保存します.次のようになります.
vector dest;
generate_n(back_inserter(dest),10,SuccessiveNumGen(3));

ここではdestを用いて数値を格納し,関数オブジェクトSuccessiveNumGen(3),すなわち開始数3を初期化した.generate_n関数は10回自動的に呼び出され,結果はdestコンテナに格納される.ここではback_も使いましたInserter関数です.これは関数アダプタです.関数オブジェクトを返し、結果をコンテナdestの末尾に配置するたびに保証します.関数アダプタは後で紹介しますが、ここではあまり言いません.私たちはそれが私たちが数字を保存する場所を提供しただけでいいです(dest.end()を直接使用してはいけません!).このようにして、最終容器の内容は3,4,5,6,......,12である.異なる開始数を使用して異なる関数オブジェクトを初期化することで、異なる数値シーケンスを生成できます.
また、関数オブジェクトには非常に重要な用途があります.すなわち述語関数として(Predicate).述語関数は、通常、渡されたパラメータを判断し、ブール値を返すために使用されます.標準ライブラリには、find_if、remove_ifなど、ユーザーによって提供される述語関数を含む複数のリロードバージョンが定義されています.ここで、最初の10以上の数値を見つける一連の数値があるとします.次のように定義できます.対応するクラス:
class NoLess
{
public:
	NoLess(int min = 0):m_min(min){}
	bool operator() (int value) const
	{
		return value >= m_min;
	}
	
private:
	int m_min;
};

こうしてfind_を呼び出すif関数:find_if(dest.begin(),dest.end()、NoLess(10))でよい.この関数は、関数オブジェクトにtrueの値を返す最初の位置(この例では10に対応する反復器位置)を返す.
関数オブジェクトに使用される標準ライブラリアルゴリズムを呼び出す場合、テンプレートタイプが「参照転送」であることを明示的に指定しない限り、そうでなければ、デフォルトでは関数オブジェクトは「値で渡す」「そのため、内部状態を持つ関数オブジェクトが渡された場合、変更された状態は関数内部にコピーされた一時的なオブジェクトであり、関数が終了すると消えてしまいます.実際に渡された関数オブジェクトの状態は変更されます.簡単なテストプログラムを書いて検証します.この例では、数値シーケンスの3番目の数値を検索します.
class Nth
{
public:
	Nth(int n=0):m_nth(n),m_count(1){}
	bool operator() (int)
	{
		return m_count++ == m_nth;
	}
	
	int GetCount()const
	{
		return m_count;
	}
	
private:
	int m_nth;
	int m_count;
};

テストは次のとおりです.
Nth nth(3);
vector::iterator nthItr = find_if(dest.begin(),dest.end(),nth);  //dest       :3,4,5,6,……,12
cout<

出力結果は、確かに3番目の数字(5)が見つかりますが、nthの状態を確認すると、返されるm_countは0のままです.nthが変更されていないことを示します.
一般的に、値を伝えるか引用を伝えるかは、私たちにあまり影響を与えません.しかし、この点を明らかにする必要があります.困ったことに、特定の実現に対してわけのわからない問題が発生することがあります.以下の例で説明します.
我々はremoveを通じてif関数は、次のように、1つの数値シーケンスの3番目の数値を削除します.
vector vec;
for(vector::size_type i=0; i<10; ++i)
	vec.push_back(i+1);

Nth nth(3);

remove_if(vec.begin(),vec.end(),nth);
	
for(vector::size_type i=0; i<10; ++i)
{
	cout<

普通の考えでは3番目の要素を削除した後の出力は、1,2,4,5,6,7,8,9,10,10である必要があります.(最後になぜ2回10が現れるのかというと、これは標準アルゴリズムライブラリの設計思想と関係がある:アルゴリズムライブラリは決して容器の大きさを変えない!いわゆるremoveは概念的なremoveにすぎず、削除された要素は後ろに捨てられ、戻り値で位置を決めることができる.標準アルゴリズムライブラリは後で説明するが、この部分のポイントではない).しかし実際の状況は驚く:1,2,4,5,7,8,9,10,9,10.
このような現象の原因は標準ライブラリの実現と関係があり、『The C++Standard Library,A tutorial and reference』という本では、著者はこのような状況をもたらす可能性のある実現を示した.
template 
ForwIter remove_if(ForwIter beg, ForwIter end, Predicate op)
{
	beg = std::find_if(beg, end, op);
	if (beg == end) 
	{
		return beg;
	}
	else 
	{
		ForwIter next = beg;
		return remove_copy_if(++next, end, beg, op);
	}
}

この実装では,関数オブジェクトopがfind_に2回パラメータとして伝達されることを明確に示した.ifとremove_copy_if関数では、標準ライブラリのデフォルトの「値別転送」により、2回転送されたopが最も初期の状態、すなわちm_countが0になります!これにより、remove_if関数に転送されたopは+next位置(nextは最初に見つかった削除される3番目の位置)から3番目の要素を再び削除し、6番目の要素も削除されます.
さらに、C++リファレンスマニュアルのWebサイトC++リファレンスマニュアルでは、次のような可能性があります.
template
ForwardIt remove_if(ForwardIt first, ForwardIt last, 
                          UnaryPredicate p)
{
    ForwardIt result = first;
    for (; first != last; ++first) {
        if (!p(*first)) {
            *result++ = *first;
        }
    }
    return result;
}

明らかに、このような実装は2回削除することはありません.私のテストを経て、結果は確かに私たちが望んでいる:1,2,4,5,6,7,8,9,10,10です.
この例では、ブール値を返すすべての関数オブジェクトが述語関数として適しているわけではありません.たとえば、この例では、異なる標準ライブラリの具体的な実装によって異なる結果が得られます.したがって,良いプログラミング習慣として述語関数の関数オブジェクトとして用いられ,その判断結果は内部変動の状態に依存しないことが望ましい.
(続き)