カテゴリー別アーカイブ: 標準ライブラリ

std::set の比較関数をラムダ式で指定するには?

std::set オブジェクトに格納された要素は operator<() を使って順序付けされるので、オブジェクトのクラスでそれが定義されていないといけない。vector や、自分で作って型のオブジェクトを格納したい場合は、operator<(const T&, const T&) 関数を自分で定義するか、set の第2テンプレートパラメータに比較ファンクタを指定する。

template < class T,                        // set::key_type/value_type
           class Compare = less<T>,        // set::key_compare/value_compare
           class Alloc = allocator<T>      // set::allocator_type
           > class set;

以下の様な構造体がある時、

struct Person {
  std::string m_name;
  double m_height;
};

比較ファンクタは、以下のように定義できる。

struct Compare {
  bool operator()(const Person& lhs, const Person & rhs) {
    return lhs.m_name < rhs.m_name;
  }
};

このファンクタを使えば、set オブジェクトを以下のように宣言することが出来る。

std::set<T, Compre> s;  // 比較ファンクタを指定した set オブジェクトの宣言

で、このファンクタをラムダ式で書いてみたかったのだが、うまくいかず困ってたところ、ツイッターで @9610n 氏にその方法をご教授していただいたので、忘れないよう本稿にメモしておく。

比較関数をラムダ式で書くと以下のようになる。

[](const Person &lhs, Person &rhs) { return lhs.m_name > rhs.m_name; };

で、これを set のテンプレートパラメータに指定してもエラーとなる。

std::set<Person, ラムダ式> s;  //  コンパイルエラーとなる

最初に書いた std::set の定義を見ればわかるように、テンプレートの第2引数はクラスである。しかるにラムダ式はオブジェクトなので、当然エラーとなるわけだ。

そこで、delctype(ラムダ式) を使って、型に変換?するといいのではないかと思えるが、それもエラーとなる。

std::set<Person, decltype(ラムダ式)> s;  //  コンパイルエラーとなる

というわけで、set のテンプレートパラメータに指定するのは std::function にするといいようだ。

std::set<Person, std::function<bool (const Person&, const Person&)>> s;
s.insert(Person{"abc", 170});
s.insert(Person{"xyz", 180});

が、上記コードはビルドは通るものの、実行時にエラーになってしまう。
なぜなら、比較関数の型を指定しただけで、その実体をどこにも指定していないからだ。

そうだったら、最初の方に示したファンクタの場合はなぜうまくいくのかと思われるかもしれないが、デフォルトコンストラクタの宣言をよく見ると、下記のようになっている。

explicit set (const key_compare& comp = key_compare(),
              const allocator_type& alloc = allocator_type());

デフォルトコンストラクタの第1引数は、比較関数オブジェクトで、省略した場合は、第2テンプレート引数 key_compare の operator() が呼ばれるようになっている。
だから、第2テンプレート引数にファンクタを指定した場合は、コンストラクタの引数を無しにしてもうまく行っていたのだ。

というわけで、以下のように記述するとよい、ということになる。

auto f = [](const Person &lhs, Person &rhs) { return lhs.m_name > rhs.m_name; };
std::set<Person, std::function<bool (const Person&, const Person&)>> s(f);
s.insert(Person{"abc", 170});
s.insert(Person{"xyz", 180});

※ 式が長くなりすぎるので、ラムダ式の部分は分離しておいた。

通常、ラムダ式を使うメリットは記述が簡潔でわかりやすくなる場合が多い、ということだ。だが、この記述をするくらいなら昔からあるファンクタの方が簡潔でわかりやすい気もしている。

みなさんはどう思うであろうか?