Fool Pool

ハマった記

C++ 条件を満たすオブジェクトを検索する

※ 注: このエントリにあるコードを動かすには、C++11 が必要。

仕事で C++ のコードを保守することになり、かれこれ5年ぶりに C++ のコードを読んだ。
そしたら、次のようなコードに行き当たって衝撃を受けた(悪い意味で)。

コード1: まずいコード

#include <iostream>
#include <vector>
#include <boost/format.hpp>
using namespace std;

class Foo {
public:
	Foo(int x, int y) : _x(x), _y(y) {};
	int x() const { return _x; }
	int y() const { return _y; }
	string to_str() { return str(boost::format("(%s, %s)") % _x % _y); }
	bool operator == (Foo other) { return this->x() == other.x(); }
        // [!] 検索のためだけにイコール演算子を再定義?!
private:
	int _x;
	int _y;
};

int main(int argc, char **argv) {
	vector<Foo> fv;

	fv.push_back(Foo {3,5});
	fv.push_back(Foo {7,8});
	fv.push_back(Foo {6,8});

	Foo key{3, 4};
	auto found = find(std::begin(fv), std::end(fv), key);
        // もちろん、検索はできるけど・・・。

	if (found != std::end(fv)) {
		cout << "Found: " << (*found).to_str() << '\n';
	}
	else {
		cout << "Not found\n";
	}

        // [!] Foo {3, 4} と Foo {3, 5} が同じになってしまう 
        if (Foo {3, 4} == Foo {3, 5}) {
                cout << "Foo {3, 4} equals Foo {3, 5}.\n";
        }
}

コード1の出力:

Found: (3, 5)
Foo {3, 4} equals Foo {3, 5}.

まさか、「リストから条件に合ったものを検索する」だけのために演算子再定義するとは。後で Foo オブジェクトをきちんと比較したくなった時はどうするつもりだったのか? こういった急場しのぎの対処を放置すると、後で保守する人が混乱するので問題だ(せめて、コメントを残してあげよう)。

代替としては、find_if を使うやり方と、for 文を回して自分でマッチングの処理を書くやり方がある。

例1(find_if):

	auto found = find_if(std::begin(fv), std::end(fv),
			[key] (Foo f) {
				return f.x() == key.x();
			});

例2(for文):

	boost::optional<Foo> found;
	for (Foo foo : fv) {
		if (key.x() == foo.x()) {
			found = foo;
		}
	}

例1の注意としては、Foo::x() を constメンバ関数として定義していない場合、 以下のようなコンパイルエラーが出る。

../sample.cpp:74:21: error: member function 'x' not viable: 'this' argument has type 'const Foo', but function is not marked const

つまり、ラムダ式の [...] 内の key はコピーキャプチャなので、状態を変更しても無意味だ。にもかかわらず、状態を変更しうる(constマーカーのない)メソッド Foo::x() を呼ぼうとしたので、エラーとなった。つまり、コピーキャプチャとして宣言したオブジェクトに対しては、状態を変更しない(constメソッドしか呼べない仕様になっているらしい。よくできた仕様だ(メッセージが分かりやすければ最高だけど)。

例2の boost::optional はけっこうイイ。perl のように、if文の条件式中にオブジェクトを入れて存在確認ができる。ポインタを意識しなくて良いのがいい。

	if (found) {
		cout << "Found: " << (*found).to_str() << '\n';
	}
	else {
		cout << "Not found\n";
	}

(boost::optional を使うときは boost/optional.hpp をインクルードする。)

ところで、「ラムダ式の先頭にキャプチャ変数を書く」という C++11 の流儀はあまり慣れない。素直にラムダの引数として渡したいのだが、find_if は1引数の関数しか受け付けてくれない。2引数の関数を1引数の関数に変換する方法はないものだろうか?

こういうときは、カリー化を使う。functional ライブラリで提供されている。

まず、binary_function を使って2引数の関数オブジェクトを作り、find_if の引数として渡すときにbind2nd 関数を使って 2 引数関数を 1 引数関数に変換する。この例のように、もとの関数の引数のいくつかを与えて、引数が少ない新たな関数を生成することを「カリー化」という。

「a と b で属性 x が等しい」ことを表現した関数オブジェクト:

class pred_with_same_x: public binary_function<Foo, Foo, bool> {
public:
	bool operator()(Foo f1, Foo f2) const
	{
		return f1.x() == f2.x();
	}
};

main のコード:

	pred_with_same_x same_x; // 2変数関数オブジェクトの生成
	auto found = find_if(std::begin(fv), std::end(fv), bind2nd(same_x, key)); // 1変数にカリー化

それにしても、昔から C++ は泥臭い処理を書かないといけないイメージがあってずっと敬遠していたが、ラムダに高階関数までできたとは。5年の間に進歩したものだ!
(でもやはり、使えるなら JavaC# を使いたい自分がいる。)

ちなみに、上であげたコードは C++11 に対応したコンパイラでないと動かない。コンパイラの引数で「-std=c++0x」などと指定する。

参考文献

C++ラムダ式は[1]で詳しく説明されている。

[1] C++0x ラムダ式 - Faith and Brave - C++で遊ぼう