web-dev-qa-db-ja.com

C ++ 11 std :: set lambda比較関数

カスタム比較関数を使用して_std::set_を作成します。 operator()を使用してクラスとして定義できますが、使用される場所でラムダを定義する機能を楽しみたいので、クラスのコンストラクターの初期化リストでラムダ関数を定義することにしましたメンバーとして_std::set_を持っています。しかし、ラムダの型を取得できません。先に進む前に、例を示します。

_class Foo
{
private:
     std::set<int, /*???*/> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};
_

検索後に2つのソリューションを見つけました。1つは_std::function_を使用しています。セットした比較関数の型をstd::function<bool (int, int)>にして、私がやったようにラムダを渡してください。 2番目の解決策は、_std::make_pair_のようなmake_set関数を作成することです。

解決策1:

_class Foo
{
private:
     std::set<int, std::function<bool (int, int)> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};
_

解決策2:

_template <class Key, class Compare>
std::set<Key, Compare> make_set (Compare compare)
{
     return std::set<Key, Compare> (compare);
}
_

問題は、あるソリューションを他のソリューションよりも優先する正当な理由があるかどうかです。私は最初の方が標準機能を使用するので好むのですが(make_setは標準関数ではありません)、私は疑問に思います:_std::function_を使用するとコードが(潜在的に)遅くなりますか?つまり、コンパイラが比較関数をインライン化する可能性を低くするのか、それとも_std::function_ではなくラムダ関数型であるのとまったく同じように動作するのに十分スマートでなければなりません(この場合は知っています)ラムダ型にすることはできませんが、私は一般的に尋ねています)?

(私はGCCを使用していますが、一般的なコンパイラが一般的に行うことを知りたいです)

要約、私がたくさんの素晴らしい答えを手に入れた後:

速度が重要な場合、最善の解決策は、operator()別名ファンクターを持つクラスを使用することです。コンパイラーは、間接化を最適化して回避するのが最も簡単です。

C++ 11機能を使用して、メンテナンスを容易にし、より優れた汎用ソリューションを使用するには、_std::function_を使用します。それはまだ高速で(ファンクターより少し遅いが、無視できるかもしれません)、任意の関数-_std::function_、ラムダ、呼び出し可能なオブジェクトを使用できます。

関数ポインターを使用するオプションもありますが、速度の問題がない場合は_std::function_の方が良いと思います(C++ 11を使用している場合)。

ラムダ関数を他のどこかで定義するオプションがありますが、比較関数がラムダ式であることから何も得られません。これは、operator()を持つクラスにすることもでき、定義の場所はとにかくセット構造になります。

委任の使用など、さらに多くのアイデアがあります。すべてのソリューションのより詳細な説明が必要な場合は、回答をお読みください:)

はい、_std::function_はsetにほとんど避けられない間接参照を導入します。理論的には、コンパイラは常にsetの_std::function_のすべての使用が、常にまったく同じラムダであるラムダでの呼び出しを伴うことを理解できます。

壊れやすい。なぜなら、その_std::function_へのすべての呼び出しが実際にラムダへの呼び出しであることをコンパイラーが証明する前に、_std::set_へのアクセスが_std::function_に何も設定しないことを証明する必要があるからしかし、あなたのラムダ。つまり、すべてのコンパイル単位で_std::set_に到達し、それらのいずれも実行しないことを証明するために、考えられるすべてのルートを追跡する必要があります。

これは場合によっては可能かもしれませんが、比較的無害な変更は、たとえコンパイラがそれを証明できたとしても、それを壊す可能性があります。

一方、ステートレスoperator()を持つファンクターは振る舞いを証明するのが簡単で、それを伴う最適化は日常的なものです。

そのため、実際には、_std::function_が遅くなる可能性があると思います。一方、_std::function_ソリューションは_make_set_のソリューションよりも保守が容易であり、プログラムのパフォーマンスのためにプログラマーの時間を交換することはかなり代替可能です。

_make_set_には、そのようなsetの型を_make_set_の呼び出しから推測する必要があるという重大な欠点があります。多くの場合、setは永続的な状態を保存しますが、スタック上で作成したものではなく、スコープから外れます。

静的またはグローバルステートレスラムダauto MyComp = [](A const&, A const&)->bool { ... }を作成した場合、std::set<A, decltype(MyComp)>構文を使用して、永続化できるがコンパイラにとって最適化が容易なsetを作成できます。 (decltype(MyComp)のすべてのインスタンスはステートレスファンクターであるため)およびインライン。 setstructに固定しているため、これを指摘します。 (または、コンパイラはサポートしますか

_struct Foo {
  auto mySet = make_set<int>([](int l, int r){ return l<r; });
};
_

これは驚くべきことだ!)

最後に、パフォーマンスが心配な場合は、_std::unordered_set_の方がはるかに高速である(コンテンツを順番に繰り返すことができず、適切なハッシュを作成/検索する必要がある)ことと、ソートされた_std::vector_は、2フェーズの「すべてを挿入」してから「コンテンツを繰り返しクエリする」場合に適しています。それを最初にvectorに詰め、次にsortuniqueeraseに詰めてから、無料の_equal_range_アルゴリズムを使用します。

コンパイラがstd :: function呼び出しをインライン化できる可能性は低いですが、ラムダをサポートするコンパイラは、ファンクタが_std::function_で隠されていないラムダである場合を含め、ファンクタバージョンをほぼ確実にインライン化します。

decltypeを使用して、ラムダのコンパレータタイプを取得できます。

_#include <set>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
   auto comp = [](int x, int y){ return x < y; };
   auto set  = std::set<int,decltype(comp)>( comp );

   set.insert(1);
   set.insert(10);
   set.insert(1); // Dupe!
   set.insert(2);

   std::copy( set.begin(), set.end(), std::ostream_iterator<int>(std::cout, "\n") );
}
_

どの印刷:

_1
2
10
_

ライブで実行されるのを見る Colir

28
metal

ステートレスラムダ(つまり、キャプチャのないラムダ)は、関数ポインタに減衰する可能性があるため、型は次のようになります。

std::set<int, bool (*)(int, int)> numbers;

それ以外の場合は、make_set 解決。非標準であるために1行の作成関数を使用しない場合、多くのコードを記述できません。

5
Jonathan Wakely

プロファイラーをいじった経験から、パフォーマンスと美しさの最良の妥協点は、次のようなカスタムデリゲートの実装を使用することです。

https://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11

std::functionは通常少し重いです。私はそれらを知らないので、あなたの特定の状況についてはコメントできません。

1
user1095108

クラスメンバーとしてsetを使用し、コンストラクター時にコンパレーターを初期化することに決めた場合、少なくとも1レベルの間接参照は避けられません。コンパイラーが知る限り、別のコンストラクターを追加できることを考慮してください。

_ Foo () : numbers ([](int x, int y)
                   {
                       return x < y;
                   })
 {
 }

 Foo (char) : numbers ([](int x, int y)
                   {
                       return x > y;
                   })
 {
 }
_

タイプFooのオブジェクトを取得すると、setのタイプは、どのコンストラクターがコンパレーターを初期化したかに関する情報を保持しないため、正しいラムダを呼び出すには、ランへの間接参照が必要です。選択されたラムダoperator()

キャプチャレスラムダには適切な変換関数があるため、キャプチャレスラムダを使用しているため、関数ポインタ型bool (*)(int, int)をコンパレータ型として使用できます。もちろん、これには関数ポインターを介した間接指定が含まれます。

1
ecatmur

違いは、コンパイラの最適化に大きく依存します。 std::functionでラムダを最適化する場合、それらは同等です。そうでない場合は、前者で間接性を導入しますが、後者ではありません。

0
filmor