C++でLINQ!! (cpplinq.hppを使ってみた!)
C#のLinqとかJavaのStream APIとか、何かと関数型言語の機能が既存言語に組み入れられるケースが増えてますね。
いつも後れを取ってしまうC++ですが、C#のLinqみたいなことがC++でもできないかな〜と探してみると、ありました。
ヘッダファイル1つインクルードするだけで、C++でLinqっぽく処理が書けますね!
ライセンスもMsPLです。素敵です。
サンプルやドキュメントはまだ充実していないようですが、勉強を兼ねてコード読みながら使い方を調べてみました。
公式サンプルを試してみる
まずは公式のサンプルを少し変更して実行してみます。
環境は、gcc 4.8.2 / Ubuntu 14.04 LTS x86_64 です。
// 配列に含まれる偶数の和を求める。
using namespace cpplinq;
int ints = {1,2,3,4,5,6};
auto result = from_array (ints)
>> where ((int i) {return i % 2 ==0;}) // 偶数にフィルタ
>> sum (); // 総和
std::cout << "sum = " << result << std::endl;
実行結果
sum = 12
おおっ!!
1:1の射影変換(select)
Linqのキモ、射影変換も試してみました。
公式のサンプルではto_vectorとかto_listしてコンテナ化しているようですが、せっかくの遅延評価を無駄にしないために、コッソリとfor_eachが提供されていました(^^
せっかくなので、コレを使ってみます。
using namespace cpplinq;
int ints = {1,2,3,4,5,6};
auto result = from_array (ints)
>> where ((int i) {return i % 2 == 0;}) // 偶数にフィルタリング
>> select ((const int n) {
return n * 10; // 10倍にする
});
std::cout << "result_array = [";
result >> for_each((const int& n){
std::cout << " " << n << " ";
});
std::cout << "]" << std::endl;
実行結果
result_array = [ 20 40 60 ]
遅延評価と言っても、シーケンス全体の確定が必要なもの(orderbyとかreverseとか)を挟むと、内部で一旦シーケンスを確定させる定格評価を行っているようでした。
なので、where/selectなどの記述の順番で処理効率が大きく異なってくるっぽいです。
1:nの射影変換(select_many)
Haskellのモナドっぽいことをするにはやはりコレですね。この機能が本命です。
公式のドキュメントにサンプルがないのが気になりますが、とりあえず試してみました。
select_manyの場合は、与える関数からfrom_xxxで作成したrangeをreturnしてあげる必要があるようです。
ここではselect_manyに与えているラムダ式の内部でrangeを作って返します。
この際、returnするrangeで参照するコンテナがスコープアウトして無効になってしまうようなときは、from_copyで内部に参照元コンテナを包含したrangeを返すようにするみたいです。
from_copyで作られるrangeは、C#のIEnumerable<T>とほぼ等しい位置づけと言っても良さそうですね。
from_copyはmoveもcopyも受け付けます。便利です。
// 見つけた偶数と、偶数を10倍/100倍したシーケンスを返す
using namespace cpplinq;
int ints = {1,2,3,4,5,6};
auto result = from_array(ints)
>> where ((const int& i) {return i % 2 ==0;}) // 偶数にフィルタリング
>> select_many ((const int n) {
std::vector<int> ret;
ret.push_back(n);
ret.push_back(n * 10);
ret.push_back(n * 100);
//コンテナがスコープ外になるので、コンテナ内包型のrangeを返す
// std::moveしない場合はコンテナがcopyされる.
return from_copy(std::move(ret));
});
std::cout << "result_array = [";
result >> for_each((const int& n){
std::cout << " " << n << " ";
});
std::cout << "]" << std::endl;
実行結果
result_array = []
あれれ?
うーん、これは cpplinq.hpp のfrom_copy_rangeクラスのコンストラクタのバグですかね。。
コンテナをcopy/moveした後にiteratorを取ってしまってるみたい。
ということで、workaroundを入れてみました。
cpplinq_from_copy_range_workaround.patch
--- a/cpplinq.hpp
+++ b/cpplinq.hpp
@@ -534,36 +534,44 @@ namespace cpplinq
container_type&& container
)
: container (std::move (container))
- , current (container.begin ())
- , upcoming (container.begin ())
- , end (container.end ())
{
+ upcoming = current = this->container.begin();
+ end = this->container.end();
}
CPPLINQ_INLINEMETHOD from_copy_range (
container_type const & container
)
: container (container)
- , current (container.begin ())
- , upcoming (container.begin ())
- , end (container.end ())
{
+ upcoming = current = this->container.begin();
+ end = this->container.end();
}
CPPLINQ_INLINEMETHOD from_copy_range (from_copy_range const & v)
: container (v.container)
- , current (v.current)
- , upcoming (v.upcoming)
- , end (v.end)
{
+ iterator_type s_b = v.container.begin();
+ auto d_c = std::distance(s_b, v.current);
+ auto d_u = std::distance(v.current, v.upcoming); // upcoming is never before current.
+ current = container.begin();
+ std::advance(current, d_c);
+ upcoming = current;
+ std::advance(upcoming, d_u);
+ end = container.end(); // end must re-create from new container.
}
CPPLINQ_INLINEMETHOD from_copy_range (from_copy_range && v) CPPLINQ_NOEXCEPT
- : container (std::move (v.container))
- , current (std::move (v.current))
- , upcoming (std::move (v.upcoming))
- , end (std::move (v.end))
{
+ iterator_type s_b = v.container.begin();
+ auto d_c = std::distance(s_b, v.current);
+ auto d_u = std::distance(v.current, v.upcoming); // upcoming is never before current.
+ container = std::move(v.container);
+ current = container.begin();
+ std::advance(current, d_c);
+ upcoming = current;
+ std::advance(upcoming, d_u);
+ end = container.end(); // end must re-create from new container.
}
template<typename TRangeBuilder>
これで再度ビルドして実行すると、
result_array = [ 2 20 200 4 40 400 6 60 600 ]
うまくいきました!
from_copyで使えるコンテナには、vectorの他にlistやdequeなど一般的なSTLのコンテナが使えます。
ただ、これとは別原因でVC10ではビルド通らないっぽいです。
もうちょっと調査が必要だな。。
でも、ウチにはVisualStudioはおろかWindows OSなんて高価なものは無いです。。
会社で試すしかないか・・(^^;;
(2014/06/05 追記)
VC10でビルドが通らない件ですが、以下の2点が原因だった模様。
VCのメモリリークチェックのnew置き換えコード
// リークしているメモリがAllocされた箇所の行番号とメモリ割当番号を表示する。
#define CRTDBG_MAP_ALLOC
#include
#include
// for malloc→_malloc_dbgの置き換え #ifdef _DEBUG
#define _REPLACED_NEW_OPERATOR_ // 置き換えのマーキング
#define new ::new(_NORMAL_BLOCK, __FILE__, __LINE__) // newの置き換え
#pragma message("-- (DEBUG_BUILD) : the 'new' macro has defined for memory leak check --")
#endif
cpplinq.hpp内で、placement newを使っている個所があるため、上記のnew置き換えとバッティングしてました。
なので、cpplinq.hppには置き換えされていない生のままのnewを使わせることで対処。
#ifdef new
#pragma push_macro("new")
#define NEW_PUSHED
#undef new
#endif
#include <cpplinq.hpp>
#ifdef NEW_PUSHED
#pragma pop_macro("new")
#undef NEW_PUSHED
#endif
複数行のラムダ関数の戻値型推論が正常動作しない
複数行のラムダ関数を使う場合、VC10だと戻値の型が正しく推論されないっぽい。
なので、自分で戻値の型を明示してやる必要があるようですね。
>> select_many ([](const int n) -> decltype(from_copy(std::vector<int>())) {
using namespace cpplinq;
std::vector<int> arry;
arry.push_back(n);
arry.push_back(n * 10);
arry.push_back(n * 100);
return from_copy(arry);
});
これで無事にVC10でもビルド/動作確認できました。