ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

C++でLINQ!! (cpplinq.hppを使ってみた!)

C#LinqとかJavaのStream APIとか、何かと関数型言語の機能が既存言語に組み入れられるケースが増えてますね。

いつも後れを取ってしまうC++ですが、C#LinqみたいなことがC++でもできないかな〜と探してみると、ありました。

Linq for 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でもビルド/動作確認できました。