Boost Signals ライブラリでは、()演算子がオーバーロードされています。
これはC++の規則ですか?コールバックなどのために?
私は同僚(たまたま大きなBoostファンである)のコードでこれを見ました。そこにあるすべてのBoostの良さのうち、これは私を混乱させるだけです。
この過負荷の理由に関する洞察はありますか?
Operator()をオーバーロードするときの主な目標の1つは、ファンクターを作成することです。ファンクターは関数のように機能しますが、ステートフルであるという利点があります。つまり、呼び出し間で状態を反映したデータを保持できるということです。
簡単なファンクターの例を次に示します。
struct Accumulator
{
int counter = 0;
int operator()(int i) { return counter += i; }
}
...
Accumulator acc;
cout << acc(10) << endl; //prints "10"
cout << acc(20) << endl; //prints "30"
ファンクターは汎用プログラミングで頻繁に使用されます。多くのSTLアルゴリズムは非常に一般的な方法で記述されているため、独自の関数/ファンクターをアルゴリズムにプラグインできます。たとえば、アルゴリズムstd :: for_eachを使用すると、範囲の各要素に操作を適用できます。そのようなものを実装することができます:
template <typename InputIterator, typename Functor>
void for_each(InputIterator first, InputIterator last, Functor f)
{
while (first != last) f(*first++);
}
このアルゴリズムは関数によってパラメータ化されているため、非常に汎用的であることがわかります。 operator()を使用することにより、この関数ではファンクターまたは関数ポインターを使用できます。以下に両方の可能性を示す例を示します。
void print(int i) { std::cout << i << std::endl; }
...
std::vector<int> vec;
// Fill vec
// Using a functor
Accumulator acc;
std::for_each(vec.begin(), vec.end(), acc);
// acc.counter contains the sum of all elements of the vector
// Using a function pointer
std::for_each(vec.begin(), vec.end(), print); // prints all elements
Operator()のオーバーロードに関する質問については、可能です。メソッドのオーバーロードの基本的なルールを尊重する限り、いくつかの括弧演算子を持つファンクターを完全に書くことができます(例えば、戻り値の型のみのオーバーロードは不可能です)。
クラスを関数のように動作させることができます。私は呼び出しが関数であるべきロギングクラスでそれを使用しましたが、クラスの特別な利点が欲しかったです。
このようなもの:
logger.log("Log this message");
これになります:
logger("Log this message");
多くの人は、ファンクターが単純な古い関数よりも優れている理由を1つ説明せずに、ファンクターを作成すると答えています。
答えは、ファンクターが状態を持つことができるということです。加算機能を検討してください-積算合計を維持する必要があります。
class Sum
{
public:
Sum() : m_total(0)
{
}
void operator()(int value)
{
m_total += value;
}
int m_total;
};
ファンクターは関数ではないため、オーバーロードすることはできません。
operator()のオーバーロードを使用して「ファンクター」(関数のように呼び出すことができるオブジェクト)を作成しているのに、同僚は正しいです。 「関数のような」引数を期待するテンプレートと組み合わせると、オブジェクトと関数の区別があいまいになるため、これは非常に強力です。
他のポスターが言ったように:ファンクターは状態を持つことができるという点で単純な関数よりも利点があります。この状態は、1回の繰り返し(たとえば、コンテナ内のすべての要素の合計を計算するため)または複数回の繰り返し(たとえば、特定の条件を満たす複数のコンテナ内のすべての要素を見つけるため)で使用できます。
コードでstd::for_each
、std::find_if
などをより頻繁に使用し始めると、()演算子をオーバーロードする機能があると便利な理由がわかります。また、ファンクターとタスクは、派生クラスの他のメソッドの名前と競合しない明確な呼び出しメソッドを持つことができます。
C++ faqのMatrixの例 を見ることもできます。それを行うための良い用途がありますが、もちろんそれはあなたが達成しようとしているものに依存します。
ファンクターは基本的に関数ポインターのようなものです。これらは一般に(関数ポインターのように)コピー可能にすることを目的としており、関数ポインターと同じ方法で呼び出されます。主な利点は、テンプレート化されたファンクターで動作するアルゴリズムがある場合、operator()への関数呼び出しをインライン化できることです。ただし、関数ポインターは依然として有効なファンクターです。
C++で functors を形成するためのoperator()の使用は、通常、同様の概念を使用する 関数型プログラミング パラダイムに関連しています: closures 。
私が見ることができる1つの強みですが、これは議論することができますが、operator()のシグネチャは異なる型で同じように見え、振る舞います。メンバーメソッドreport(..)を持つクラスReporterがあり、メンバーメソッドwrite(..)を持つ別のクラスWriterがある場合、おそらく両方のクラスを使用する場合はアダプターを記述する必要があります。他のシステムのテンプレートコンポーネント。気にするのは、文字列または何を渡すかだけです。 operator()を使用せずにオーバーロードするか、特殊なタイプのアダプターを記述すると、次のようなことができなくなります。
T t;
t.write("Hello world");
tには、const char *(またはconst char [])に暗黙的にキャスト可能なものをすべて受け入れるwriteというメンバー関数が必要であるためです。この例のReporterクラスにはそれがないため、T(テンプレートパラメーター)をReporterにするとコンパイルに失敗します。
しかし、これまでのところ、これはさまざまなタイプで機能することがわかります
T t;
t("Hello world");
ただし、型Tにはそのような演算子が定義されていることが明示的に必要であるため、Tに対する要件がまだあります。個人的には、ファンクターは一般的に使用されているのでファンクターにはあまり向いていないと思いますが、この動作。 C#のような言語では、デリゲートを渡すことができます。私はC++のメンバー関数ポインターにはあまり詳しくありませんが、同じ動作を実現できると想像できます。
シンタックスシュガーの振る舞い以外に、このようなタスクを実行するためのオペレーターのオーバーロードの強さは実際にはわかりません。
私が知っているよりももっと良い理由を持っている人がもっといると確信していますが、他の人たちに分かち合うために私の意見を述べると思いました。
別の同僚は、ファンクターオブジェクトを関数として偽装する方法になり得ることを指摘しました。たとえば、これ:
my_functor();
本当に:
my_functor.operator()();
それはこれを意味します:
my_functor(int n, float f){ ... };
これをオーバーロードするためにも使用できますか?
my_functor.operator()(int n, float f){ ... };
他の投稿は、operator()がどのように機能し、なぜそれが有用であるかを説明する良い仕事をしました。
私は最近、operator()を非常に広範囲に使用するコードを使用しています。この演算子をオーバーロードすることの欠点は、一部のIDEが結果としてツールの効率が低下することです。 Visual Studioでは、通常、メソッド呼び出しを右クリックして、メソッドの定義や宣言に移動できます。残念ながら、VSはoperator()呼び出しにインデックスを付けるほど賢くありません。特に、オーバーライドされたoperator()定義が至る所にある複雑なコードでは、どのコードがどこで実行されているかを把握するのは非常に困難です。いくつかのケースでは、実際に実行されているものを見つけるためにコードを実行し、コードをトレースする必要があることがわかりました。