11

多くの標準ライブラリ アルゴリズムは述語関数を取ります。ただし、これらの述語のタイプは、任意のユーザー提供のテンプレート パラメーターです。std::function代わりに、C++11 がこれらが特定の型を取ることを指定しないのはなぜですか? 例えば:

template< class InputIt >
InputIt find_if( InputIt first, InputIt last,
             std::function<bool()> p );

引数の型としてテンプレートの代わりにこれを使用していませんか?

4

3 に答える 3

10

std::functionランタイムポリモーフィズム用です。特定のインスタンスは、任意の型 (もちろん、 のシグネチャにstd::function適した型) のファンクタを格納できます。std::function

標準ライブラリのアルゴリズムなどは、関数パラメーターの型に基づいてテンプレート化されています。そのため、ジョブを実行するためにランタイム ポリモーフィズムは必要ありません。それらはコンパイル時のポリモーフィズムに依存しています。

何よりも重要なことは、そのようなアルゴリズムは実行時のポリモーフィズムのコストを強制する必要がないということです。ランタイム ポリモーフィズムが必要な場合は、それを astd::functionまたは何でも送信できます。コンパイル時のポリモーフィズムが必要な場合は、ポリモーフィック ディスパッチを使用しない型 (別名: ほとんどのファンクターまたは関数) を提供します。

ランタイム ポリモーフィズムのコストには、関数呼び出しをインライン化できないことも含まれます。適切なファンクター (または、コンパイラーの性能によっては関数ポインター) を使用すると、コンパイラーは通常、必要に応じて関数呼び出しをインライン化できます。ランタイム ポリモーフィズムを使用すると、ランタイム ディスパッチのコスト (追加のパラメーター転送コストが含まれる場合があります) を支払うだけでなく、重要な最適化の機会も失われます。

于 2013-03-14T12:30:44.743 に答える
9

パフォーマンス!

テンプレートの基本機能は、モードよりも非常に優れていstd::functionます。私はあなたのためにこのテストをしました:

template <typename F>
void test1(const F &f)
{
    for (unsigned long long i = 0; i < 19000000; i++)
        f();
}

void test2(function<void()> &f)
{
    for (unsigned long long i = 0; i < 19000000; i++)
        f();
}

int main()
{
    {
    LARGE_INTEGER frequency, start, end;
    double interval;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&start);

    unsigned long long x = 0;
    test1([&x]()
    {
        x++;
    });

    QueryPerformanceCounter(&end);
    interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;

    cout << "Template mode: " << interval << " " << x << endl;
    }
    {
    LARGE_INTEGER frequency, start, end;
    double interval;
    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&start);

    unsigned long long x = 0;
    function<void() > f = [&x]()
    {
        x++;
    };
    test2(f);

    QueryPerformanceCounter(&end);
    interval = (double) (end.QuadPart - start.QuadPart) / frequency.QuadPart;

    cout << "std::function mode:" << interval << " " << x << endl;
    }
}

テンプレートモード: 2.13861e-006

std::関数モード:0.220006

-O2Windows7 Core2 Duo CPU 2.40GHz上の gcc 4.7.2

于 2013-03-14T12:38:46.677 に答える
4

std::functionは不完全だからです。

それはどのように不完全ですか?方法を列挙してみましょう。

{

まず、std::function渡された任意のオブジェクトの完全な転送をサポートしていません。そして、実際には、それはできません。 std::function1 つの固定署名を呼び出し元に公開し、さまざまな種類の呼び出し先を受け入れることができます。完全な転送には、呼び出し元と呼び出し先ごとにカスタム作成された署名が必要です。署名で公開する引数の完全な転送をサポートしますが、それだけでは不十分です。

std::function2 つの引数を取るa とint、a を想像してみてくださいdouble。それが完全な転送を行うためには、同じセットを受け入れる必要がありますint&(揮発int&&性のバリアントは言うまでもありません)。完全な転送を実現するためにそれぞれが受け入れる必要が ある署名の数は、引数の数に応じて指数関数的に増加します。が公開する署名のセット (現在 1 つ) はインスタンス化時に固定されますが、公開する署名のテンプレート セットは無制限であり、使用時にのみ生成されます。一部の関数のようなオブジェクトは、これらのケースに対して異なる方法で最適化するため、これは重要です! そのため、ラップされた型への呼び出しを完全に転送する機会をすべて削除しました。int const&doublestd::functionstd::functionstd::function

が不完全である2 つ目の理由std::functionは、コンパイラがダメだからです。ラムダを でラップし、std::functionそれを使用してアルゴリズムを呼び出すと、コンパイラは理論上、これstd::functionが固定ラムダをラップしていることを認識できますが、実際には、この事実を見失い、 をstd::functionジェネリック クラスのラッパーとして扱います。 . したがって、 の署名がstd::functionアルゴリズムのユースケースと正確に一致し、 の型のボトルネックがstd::function転送を不完全にするのを防ぐ場合でも、実際には によって実行される型の消去によるオーバーヘッドが発生しstd::function、コンパイラはstd::functionコール「バリア」を最適化するのは難しいと思います。

が不完全である3 つ目の理由std::functionは、アルゴリズムの作成者がアルゴリズムで渡すことができるパラメーターを過度に制限することを奨励することです。を調べるfind_ifと、探しているものはコンテナーに格納されている型と同じ型であるか、少なくとも変換可能である必要があるという単純な仮定がありますが、std::find_ifアルゴリズムは、渡されたファンクターの下でそれらが比較可能であることのみを必要とします。

これにより、複数の型を認識するファンクターを作成し、コンテナーの型とは無関係な型のターゲット オブジェクトを渡すことができ、問題なく動作します。ほとんどの人はこれを必要とせず、彼らのコードはそれがなくても機能します - これも良いことです。

素朴なstd::find_if人はコンテナの基礎となるタイプを抽出し、比較関数はそのタイプのペア間で行われます-または、コンテナタイプと検索対象のタイプとの間の4方向の比較になります。ある場合には、柔軟性が失われます。別の場合には、奇妙なコーナー ケースに誰もがお金を払います。そして C++ では、必要なときに必要な機能に対してのみ料金を支払う必要があります。

が不完全である 4 つ目の理由std::functionは、基本的に型消去のツールであることです。これらは実装の詳細ですが、それらからかけ離れたコンパイラを私は知りません。Astd::functionの目的は、単一の署名と戻り値を公開し、「この署名とこの戻り値に一致するものは何でも格納でき、呼び出すことができる」ということです。このタスクを実行するために、静的ランタイム インターフェイスと実装を公開します。を初期化するstd::functionと、コンパイル時の作業が行われ、その特定のオブジェクトを統一std::functionインターフェイスでラップするヘルパー オブジェクトが生成され、それがパターンに格納されpImplます。これらはすべて、型消去が必要ない場合は不要な作業です。

標準アルゴリズムは、手作りのソリューションとほぼ同じくらい効率的な高レベルのコードを記述することに関するものです。これらの問題のほとんどを解決するために、ポインター関数呼び出しのコストでさえ必要ありませんstd::function

}; // enum

std::functionvirtualコールバック、定型的な単一目的インターフェイスの置き換え、および呼び出し元から実装の詳細を隠す必要がある場合 (たとえば、設計上の決定のためにコンパイル ユニットの境界を越える必要がある場合)のための優れたツールです。

良いニュースは、この問題に対するより良い解決策が次々と出てきていることです。特に、C++14 または C++17 の目標の 1 つは、「このテンプレート引数には次のプロパティがある」と言うことができる、ある種の「概念」をサポートすることです。正確な構文がどうなるかは、はっきりとは言えません.C++11 コンセプトの提案はおそらくかなり進んでいます.しかし、それについて多くの熱意があり、現在、この問題に関するワーキンググループがあります.

それが完了すると、「この引数は単なる任意の型ではなく、2 つの値 (含まれるデータ型を含む) を取るファンクターである型である」という意味のある概念情報でファンクターをマークアップできるようになります。 )、bool関数のドキュメントにアクセスしなくても、コンパイラ、IDE、およびユーザーが理解できる互換性のある値を返します。

于 2013-03-14T14:37:51.137 に答える