2012年8月1日水曜日

A bit inside of Transactional Language Constructs for C++ - part.2 Intel TM ABI

前回 Part.1 では、Transactional Language Constructs for C++ (以下 TM 提案)について Atomic transaction の記述「他のスレッドが transaction の中間結果を観測しない」が、どのようなからくりになっているかを説明しました。Part.2 では、TM 提案と対とも言える文書、Intel TM ABI specification を元にコンパイラがどのように C++ TM を実現しようとするかについて覗いてみたいと思います。

TM 提案を実装しているコンパイラはいくつかありますが Intel によるプロトタイプ実装 Intel C++ STM Compiler, Prototype Edition というものがあります。Intel TM ABI とはこの実装で使用されている ABI です。元々 TM 提案は何が実現されるか(what)についてだけ規定しておりどのように実装するか(how)については規定していません。これはソフトウェア、ハードウェア、ハイブリッドといった区分を含め様々な TM 実装を利用できるようにするためです。コンパイラの実装としてもこの方針に則っており、Tntel TM ABI に則ったライブラリ(libitm)を差し替えることで実装を切り替えられるようになっています。GCC 4.7 以降も TM 提案を試験的に実装していますが、同様に Intel TM ABI に則ったライブラリ(正確には特に例外周りで変更があるみたいですが)への呼び出しに変換されて実現されます。

さて、では TM 提案 に則ったコードをコンパイラがどのように libitm への呼び出しに変換するのかを見てみましょう。以下は TM ABI の例を一部改変しています。

int s;
[[transaction_safe]] int f(int);
void foo(void)
{
  int i;
  for (i=0;i<10;i++)
  {
    __transaction_atomic {
      s += f(i);
      if (s > 1000) __transaction_cancel;
    }
  }
}

このコードは(例外関係を無視し効率を考えないとして)例えば次のようなコードに変換されます。

int s;
int f(int);
static _ITM_srcLocation start_outer_loc = {…};
static _ITM_srcLocation commit_outer_loc = {…};
static _ITM_srcLocation abort_loc = {…};
void foo(void)
{
  _ITM_transaction * td = _ITM_getTransaction();
  for (i=0;i<10;i++) {
    int doWhat = _ITM_beginTransaction (td,pr_instrumentedCode | &start_outer_loc);
    if (doWhat & a_restoreLiveVariables) {
      /* TM 化していない有効なローカル変数を復元する */
    }
    if (doWhat & a_abortTransaction) goto txn1_abort_label;
    if ((doWhat & a_saveLiveVariables)) {
      /* TM 化しない有効なローカル変数を保存する */
    }
    int sval = (int)_ITM_RU4 (td, (uint32 *)&s);
    sval += f_@TXN(i); // TM 化された関数 f の呼び出し
    _ITM_WaRU4 (td, (uint32 *)&s, (unit32_t)sval);
    if (sval > 1000)
      _ITM_abortTransaction(td,userAbort,&abort_loc);
    _ITM_commitTransaction(td,commit_outer_loc);
    txn1_abort_label:
  }
  return;
}

以下では _ITM_ を省略して記述します。srcLocation 関連はデバッグ情報みたいなもので無視して構いません。"instrumented" という表現は TM 対応化された、くらいの意味に取ればいいと思われます。では、簡単に流れに沿って説明してみましょう。

  • まず最初に getTransaction() で transaction descriptor を取得しています。以下の libitm 関数呼び出しで共通して渡されている情報です。内部に libitm で必要な情報が格納されているわけですが、結局は実質スレッドに紐付け可能なわけで libitm 側で管理すればいいんじゃね?という話はあります。この辺りも簡単に ABI spec 3.8 節に触れられており、また、実際 GCC の場合は td が存在しないコードになります。特に本筋に大きな影響はないので spec の記載通りで書いています。
  • beginTransaction() によって transaction を開始します。これは内部的に setjmp() と同じような処理を含んでおり、commit に失敗した場合や abort された場合に(longjmpと同様の処理が行われ)再びこの関数から戻ってきます。戻り値は次にどのような処理を行うべきか、です。前述の通り abort した場合などもこの関数から戻ってくるためどのような処理をするか戻り値から判別しなければなりません。下表で再実行となっているのは commit に失敗した場合の transaction 再実行です。取り消し不可能云々は本稿では説明しません。さしあたって無視してもらっても大筋は成立します。
    transaction 開始時
    状況戻り値
    直列で取り消し不可能な transaction (serial irrevocable transaction)を開始
    a_runUninstrumentedCode
    transaction 開始a_saveLiveVariables | a_runInstrumentedCode
    transaction 再実行a_restoreLiveVariables | a_runInstrumentedCode
    直列で取り消し不可能な transaction として transaction 再実行(モード変更)a_restoreLiveVariables | a_runUninstrumentedCode
    取り消し不可能な transaction として transaction を開始a_runInstrumentedCode
    transaction 終了時
    状況戻り値
    transaction が aborta_restoreLiveVariables | a_abortTransaction
  • a_restoreLiveVariables と a_saveLiveVariables はローカル変数に対するものです。libitm に渡して TM 化する場合オーバーヘッドがかかるため TM 化する変数アクセスは当然出来るだけ絞りたいわけです。transaction 中であればローカル変数は自分のスレッドからしか有効にアクセスできません。ということでローカル変数について保存・復帰によって対処しようというものがこれらのフラグとそれに対応する処理となります。
  • abort された場合は、a_abortTransaction が返ってくるので(a_restoreLiveVariables によってローカル変数の復帰済み)、以降の transaction 関連コードをスキップします。
  • RU4() だとか WaRU4() が実際に TM 化するためのメモリアクセスの置き換えです。最初の R or W が読み書きの区別、最後が型です(この場合は U4)。途中の aR 等は読み込みの後(after Read)等の意味を持ち、状況に応じて不要な処理を省くといったことを実現するために分けられています。
  • f_@TXN は関数 f() の TM 化バージョンという表記です。実際にはコンパイラによって変わってくるでしょう。transaction 中であるかを判別する inTransaction() という関数もあるのでそれを使って関数内で切り替えることも可能だと思いますが、恐らくはゼロオーバーヘッドを考慮して 2 バージョン用意する形を想定して書かれているのだと思います。
  • abortTransaction() はそのまま transaction の abort で前述のように beginTransaction() の場所に戻ります。
  • commitTransaction() もそのまま transaction の commit です。commit に失敗した場合、前述のように beginTransaction() に戻って transaction が再実行されます。

さて、どうでしょうか? まだ libitm の中まで見ていませんが、begin, abort, commit があって変数アクセスが置き換えられているということから(単一グローバルロックでなくとも)、Transactional Memory が実現できそうかなという感じがするんじゃないでしょうか? part.3 では libitm 実装の一つである RSTM を参考にして TM がどのように実現されているかを簡単に見てみたいと思います。

2012年7月31日火曜日

A bit inside of Transactional Language Constructs for C++ - part.1 Atomic transaction

Boost.勉強会 #10 にて @yohhoy さんが Transactional Language Constructs for C++ (C++ における Transactional Memory 拡張)について発表 されました。丁度自分も提案文書を読んでいるところであり Transactional Memory (以下 TM)というか並列プログラミング全般について知識がないので勉強会駆動の発表ネタになるかなと思っていたところだったので非常にタイムリーでした。発表内容としては現在提案されている TM 拡張の syntax と semantics についての説明で、実装については対象外だったわけですが(もともと提案文書には実装については含まれていない)、その発表の中で

  • (Atomic Transaction で、他の transaction だけでなく)他のスレッドに途中状態を見せないってどう実現するのか分からない

というコメントがありました(多分)。その時は、ピンと来なかったのですが色々調べたりするうちにその感覚が分かってきました。その心は

  • ゼロオーバーヘッドか?(TM 有効にしても実際に TM を使用しなければオーバーヘッドはないか?)

という質問にも通じます。私は TM 拡張は基本的に transaction 内部のコードだけ変更すればいいと考えていたので実行時にはほぼゼロオーバーヘッドだと思っていたのですが、(transaction 外の)他のスレッドにも途中状態を見せないということは transaction 外のコードに対しても何かガードなりを設ける必要があるのではないか?という懸念があった訳です。

が、なんというか「確かに嘘は書いてないんだけどさぁ」という解釈が可能であることに気付きましたので、本稿ではそのことについて書いてみます。

さて、提案文書では Atomic transaction について以下のように書かれています。

In a data-race-free program, an atomic transaction appears to execute atomically; that is, the compound statement appears to execute as a single indivisible operation whose operations do not interleave with the operations of other threads.
データ競合がないプログラムでは、atomic transaction はアトミックに実行される。(atomic transaction として指定された)複文は他のスレッドの操作とインターリーブされない単一の分割不可能な操作として実行される。

さて、ここでどうしても後半に意識がいってしまうわけですが、この文章で最も重要なのは最初の部分 "In a data-race-free program" です(appear であることも多分重要)。

data-race-free とは何かについては TM 提案の最初の方にも簡単に触れられています。C++11 では 1 スレッド内の実行順序について "sequenced before" という関係で順序が定められているものがあります。またスレッド間の実行順序について "synchronized with" という関係で順序が定められているものがあります。これらをもとに "happens before" という関係が定められています(厳密にはもうちょっと複雑)。

  • A is sequenced before B → A happens before B
  • A synchronizes with B → A happens before B
  • A happens before B ∧ B happens before C → A happens before C

そして、あるメモリ領域に対する書き込み操作と、別の読み込み操作あるいは書き込み操作が同じメモリ領域に対して行われている時、これらを conflict と称します。以上を元に data race (データ競合) は、次のように規定されています。

The execution of a program contains a data race if it contains two conflicting 2 operations in different threads, at least one of which is not an atomic operation, and neither happens before the other.
異なるスレッドの 2 つの操作が conflict しており、"happens before" の関係になく、かつ、少なくとも一方は atomic ではない場合、data race が発生している。

要するに、「同一メモリ領域に対する書き込みと、読み込みないし書き込み」(=conflict)について順番が定められていないケースを data-race と呼んでいる訳です。

さて、transaction 間については、ある transaction の終了と別の transaction の開始には "synchronized with" の関係があるとされ、実行がインターリーブしません。従って、transaction 間では data-race は発生しません。では、transaction と transaction 外ではどうなるでしょうか?

atomic transaction はロックや atomic など他の同期機構を含むことができません。このため、transaction 内のコードと transaction 外のコードについて "synchronized with" 関係が発生しません。この状況下で data-race-free、つまり順序不定な conflict が存在しないためには、

  1. 同一スレッド内で sequenced before 関係にあるか、
  2. そもそも conflict が存在しないか、
  3. 別の transaction 内に存在しているか、

のいずれかである必要があります。結果として別スレッドの transaction 外のコードは transaction の途中状態を見ることがありません。この場合、2 しか有り得ず、そもそも transaction が変更するメモリ領域に対するアクセス、conflict が存在しない訳ですから。

つまり、atomic transaction において他のスレッドが transaction の途中状態を見ないというのは atomic transaction によって保証されているというよりは、その前提、data-race-free なプログラムである、というところに依るところが大きいと言えます。data-race-free であることはプログラマが保証してやらねばなりません。一番簡単なのは conflict があるところを transaction で囲むことでしょう。この場合、atomic transaction である必要はありません(transaction 間であれば relaxed transaction でも順序が規定されるので)。

本記事では A bit inside of Transactional Language Constructs for C++ part.1 として TM 提案 での atomic transaction の記述の解釈について書いてみました。心づもりとしては part.2 では TM 提案の対とも言える文書、Intel TM ABI を元にコンパイラがどのように C++ TM を実現しようとするかについての概要、part.3 では Intel TM ABI を実装しているライブラリ RSTM の実装を簡単に覗いてみたいと思っています。

2012年7月15日日曜日

小ネタ: BOOST_CHECK_CLOSE と BOOST_CHECK_CLOSE_FRACTION

Boost.Test は Boost にあるユニットテストフレームワークである。ユニットテストの項目記述用に色々とマクロが定義されているのだが、その中に浮動小数点数比較用のものがある。なぜ浮動小数点数用が別にあるかというと誤差の問題が存在するからで許容範囲を与えるようになっている。

BOOST_CHECK_CLOSE         ( 1.00001e-10, 1.00000e-10, 0.00001 ); // FAIL
BOOST_CHECK_CLOSE_FRACTION( 1.00001e-10, 1.00000e-10, 0.00001 ); // OK

ではこの _FRACTION の有無の違いは何かということでドキュメントを見ると、見ると……。違いが分からなかったという貴方は正しい。なぜならドキュメントが間違っているからだ。間違っていることは #3964 で指摘されており trunk では修正されているのだが release ブランチにマージされていないようだ。ということで、チケットを切ってみた。状況にもよるがチケット切ると割とすぐに対応してもらえたりするので、見つけた問題点はばんばんチケット切ると良いと思う(もちろん重複確認ぐらいはした上で)。

話を戻すと、BOOST_CHECK_CLOSE はパーセンテージ指定、BOOST_CHECK_CLOSE_FRACTION は比率指定、つまり

BOOST_CHECK_CLOSE         ( 1.00001e-10, 1.00000e-10, 0.001 );   // OK
BOOST_CHECK_CLOSE_FRACTION( 1.00001e-10, 1.00000e-10, 0.00001 ); // OK

が同じ挙動になるということである。

ちなみにパーセンテージ、比率ということからも分かる通り、許容範囲は相対指定である。マクロを使う限りにおいては引数それぞれからの相対で両方を満たしていなければならない(これに関するドキュメントもリリースでは古いので trunk から)。

|u − v| / |u| ≤ ε ∧ |u − v| / |v| ≤ ε

つまり以下はいずれも FAIL する。

BOOST_CHECK_CLOSE         ( 0.99999e-10, 1.00000e-10, 0.001 );   // FAIL
BOOST_CHECK_CLOSE_FRACTION( 0.99999e-10, 1.00000e-10, 0.00001 ); // FAIL

片方からだけでも構わないという場合は第 4 引数を FPC_WEAK として直接 check_is_close を使えば良い。

2012年7月2日月曜日

emplace と aggregate

C++11 で rvalue reference による perfect forwarding が実現されたため、STL コンテナに emplace 系の関数が追加されている。insert() や push_back() は value_type 型の値自体を与える必要があるが、emplace 系には生成に必要な引数(コンストラクタの引数等)を渡してやれば良い。

 std::vector<std::pair<int, int>> v1;
 std::pair<int, int> p{ 0, 0 };
 v1.push_back(p);
 v1.push_back({ 0, 0 });
 v1.emplace_back(0, 0);

これで無駄な temporary 作成が無くなり万々歳、なのだが。非常によく似た以下の例は実はコンパイルできない。

 struct point { int x, y; };
 std::vector<point> v2;
 point pt{ 0, 0 };
 v2.push_back(pt);
 v2.push_back({ 0, 0 }); // (g++ 4.5 compilation error
                        // by ambiguous between const value_type & and value_type &&)
 v2.emplace_back(0, 0);  // g++ 4.5-4.8 compilation error
                        // new initializer expression list treated as compound expression

g++ 4.5 特有のエラーについては置いとくとして、下側なんでやねんというのは StackOverflow の質問 ならびに引用されている DR を見てもらえれば良いのだが、問題は emplace 系で呼び出される std::allocator_traits::construct(m, p, args) が最終的に ::new((void *)p) U(std::forward(args)...) となり、list-initialization ではなく direct-initialization になってしまう、という点にある。単純に list-initialization にする、というのは例えば下記のようなコードの挙動を変えてしまうということで、

 std::vector<std::vector<int>> v;
 v.emplace_back(3, 4); // v[0] == {4, 4, 4}, not {3, 4} as in list-initialization

is_constructible<U, Args...> が true かどうか(direct-initialization が有効かどうか)によって、direct-initialization か list-initialization かを呼び分ける方向での修正が提案されている。

こんな単純そうな例ですら落とし穴があるとは、ほんと C++11 難しい。

2012年3月16日金曜日

左シフト演算子と未定義動作

指定したビット数だけ下(LSB)からビットが立ったビットマスクを得るために

(1U << bits) - 1

という式を愛用していたのだがはまってしまった。bits == std::numeric_limits::digits つまり全て 1 のマスクにしたい場合、この式は未定義動作をもたらす。14882:2003 5.8/1 には(右シフトも含めて一般に)こうある。

The behavior is undefined if the right operand is negative, or greater than or equal to the length in bits of the promoted left operand.

そして実際に少なくとも x86 上ではまず期待通り(全て 1)にはならない。x86 アセンブリの左シフト命令はシフト数は 5 ビット分だけ有効である。つまり 32 == 100000(2) は 0 ビットシフトと同じになり結果として上の式は 1 - 1 == 0 となってしまう。

左シフトには他にも未定義動作となる場合があるのだがこの記述にも変遷がある。

14882:1998 と 14882:2003 では以下の通りである。つまり undefined behavior という記述がない。E1 が signed な場合はどうなるのが正しいんだよ、という感じである。恐らく未定義動作を意図していない記述だろう。

The value of E1 << E2 is E1 (interpreted as a bit pattern) left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity 2 raised to the power E2, reduced modulo ULONG_MAX+1 if E1 has type unsigned long, UINT_MAX+1 otherwise. [Note: the constants ULONG_MAX and UINT_MAX are defined in the header <climits>). ]

一方、14882:2011 (多分。自分の参照文書は n3291 なので違う場合もあり得る)では DR854 によりこうなっている。

The value of E1 << E2 is E1 (interpreted as a bit pattern) left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 multiplied by the quantity 2 raised to the power E2 × 2E2, reduced modulo ULONG_MAX+1 if E1 has type unsigned long, UINT_MAX+1 otherwise. [Note: the constants ULONG_MAX and UINT_MAX are defined in the header <climits>). ] one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1×2E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

int, long int よりも大きな型も想定した記述となっていること、E1 が signed な場合にも記述が明確となり、負の場合には未定義動作となっている。

そして、既に DR1457 が出ており以下のように修正される予定である。

if E1 has a signed type and non-negative value, and E1 × 2E2 is representable in the corresponding unsigned type of the result type, then that value, converted to the result type, is the resulting value; otherwise, the behavior is undefined.

これは、1 << std::numeric_limits::digits のように最大の負数を得るためにビット数-1(=符号ビット分少ないビット数)だけシフト、つまり符号ビットを 1 にするコードを合法にするためである。ただし、これは signed の範囲で表現できない unsigned の値から signed への変換について処理系定義の動作を含む。……しかしだったら signed 全体について全て unsigned にして処理した後 singed に戻すとか規定してしまえばいいじゃないかとも思う。

なお、右シフトについては実質的に記述は変わっておらず(文による表現が式による表現になっただけ)、E1 が負数の場合は処理系定義となっている(以下は n3291 より)。

The value of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a non-negative value, the value of the result is the integral part of the quotient of E1/2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.

2012年3月14日水曜日

図解 MPL Metafunctions

Boost ライブラリの中でも Boost.MPL は割と食わず嫌いというかなんとなく敬遠している人も多いライブラリのような気がする。MPL = Meta Programming Library ということで「メタプログラミング?黒魔術?」みたいな受け取り方をされているんじゃないかというか、実際には

という言にふさわしい、コンパイルタイムメタプログラミングを Sequence、Iterator、Algorithm といったランタイム(実行時:コンパイルタイムの対義語として)の語彙で取り扱えるようにする一般人のためのライブラリである。typename ::type 乱舞になりがちなところと、代入や状態の変更ができず関数型言語的に記述する必要があるところだけ気をつければ難しくない。のだがちょっと自分の中で整理しきれていなかったのが Metafunction 関係である。前回 MPL 用再帰的 equal を書く中で大分整理できたのでまとめてみる。

MPL の Metafunction 関係について包含図を書いてみると以下のようになる。

MPL Metafunctions

Metafunction がランタイム世界での通常の関数にあたる。適用が ::type しないといけないところがちょっと面倒なところだ。Metafunction 以外の Lambda Expression は高階関数にあたるところであり Metafunction Class と Placeholder Expression からなる。Metafunction Class は関数オブジェクト(Functor)にあたると思えばよい。operator() の代わりにメンバテンプレート apply があると考えても良いだろう。さて、ランタイムにおける世界でも毎回毎回関数オブジェクトを書くのは面倒ということで Boost.Lambda や C++11 でのラムダ式がある。これの MPL 版にあたるのが Placeholder Expression だ。良く分からない英語が書いてあるが、placeholder(boost::mpl::_1 等)自身や、テンプレートパラメータのどこかに placeholder ないし Placeholder Expression を含むテンプレートクラスの特殊化(specialization)である。

_1
plus<_, int_<2> >
if_< less<_1, int_<7> >, plus<_1,_2>, _1 >

最後が bind expression である。これは単に bind の特殊化だ。bind はメンバテンプレート apply を持つため必ず Metafunction Class となるが、必ずしも Placeholder Expression とはならない。placeholder なしで bind することも可能だからだ。そのため Placeholder Expression から一部はみ出している。

bind< times<>, int_<2>, int_<2> >

さて、なんとなく Metafunction 族については分かっただろうか。自分もここまでは大体分かっていたのだがそれに作用するもの、具体的に主要なものをあげると apply, lambda, apply_wrap, protect, quote 辺りについて整理ができていなかったので以下に並べてみる。以下の図で青色が引数に取る対象、桃色がある場合はその部分が実際に変換される対象である。

MPL apply

apply は Lambda Expression を引数に取り、その Lambda Expression を適用する(呼び出す)。実際には lambda を適用した後で apply_wrap を呼び出す。では、その lambda と apply_wrap がどうなっているかというと、

MPL lambda

lambda は任意の型を引数に取るが、その内 Placeholder Expression を Metafunction Class に変換する。他はそのままである。

MPL apply_wrap

そして、apply_wrap は Metafunction Class の呼び出しである。実際には ::apply<...> と同等だ。なので、apply は lambda により全て Metafunction Class に変換した後、apply_wrap で呼び出すことになる。

MPL protect

protect は Metafunction Class を受け取るが bind expression しか変換しない。リファレンスマニュアルの例だが以下のように bind 時の placeholder の置き換えを抑止する。protect なしだと入れ子の bind 中の _1, _2 まで置き換えられているが、protect をかけると置き換えされない。

struct f
{
    template< typename T1, typename T2 > struct apply
    {
        typedef T2 type;
    };
};

typedef bind< quote3<if_>,_1,_2,bind<f,_1,_2> > b1;
typedef bind< quote3<if_>,_1,_2,protect< bind<f,_1,_2> > > b2;

typedef apply_wrap2< b1,false_,char >::type r1;
typedef apply_wrap2< b2,false_,char >::type r2;

BOOST_MPL_ASSERT(( is_same<r1,char> ));
BOOST_MPL_ASSERT(( is_same<r2,protect< bind<f,_1,_2> > > ));

MPL quote

最後に上の protect の例でも使われていた quote である。これは Metafunction を Metafunction Class に変換する。引数の数を指定する必要があり、その際デフォルトパラメータの部分も数えなければならないし、変換された Metafunction Class には全引数を渡してやらなくてはならない。bind 等と併用するくらいなら Placeholder Expression + lambda の方が楽かもしれない。

まとめ

どうだろうか。少しでも理解の助けになっただろうか?現実的には以下のルールに従っていれば大体問題ないと思われる。

  • 高階関数を受け取る場合は Lambda Expression を受け取ることにする
    • 適用(呼び出し)する場合は apply を使う
    • 高階関数として「そのまま」渡したい場合は lambda をかける(protect をかけた状態になる。実際 lambda は Lambda Expression に対しては protect<bind<...> > になる)
  • 高階関数として Metafunction を渡したい場合は quote をかける

2012年3月12日月曜日

Boost.MPL 用再帰的 equal

Boost.MPL の Sequence はその特性上 is_same を使いにくい。例えば

BOOST_MPL_ASSERT((
 boost::is_same<
  boost::mpl::push_front<
   list<>,
   int_<1>
  >::type,
  list<int_<1> >
 >));

は通らない。push_front した結果は list ではなくなっているからだ。このため Boost.MPL には equal というアルゴリズムが用意されている。

BOOST_MPL_ASSERT((
 boost::mpl::equal<
  boost::mpl::push_front<
   list<>,
   int_<1>
  >::type,
  list<int_<1> >
 >));

equal は Sequence の種類を気にせずその「要素」が一致しているかを判別するためこれは通る。通るのだが、equal は最上位の Sequence だけしか考慮してくれないため多次元の Sequence だとうまくいかない。equal の第 3 引数は要素を比較する際の Predicate であるため次元に合わせて equal を渡してやればいいのだが割と面倒である。

// bind base
BOOST_MPL_ASSERT((
 boost::mpl::equal<
  vector<vector<int_<1> > >,
  list<list<int_<1> > >,
  boost::mpl::bind<
   boost::mpl::quote3<boost::mpl::equal>,
   boost::mpl::_1,
   boost::mpl::_2,
   boost::mpl::protect<
    boost::mpl::bind<
     boost::mpl::quote2<boost::is_same>,
     boost::mpl::_1,
     boost::mpl::_2
    >
   >
  >
 >));
// lambda base
BOOST_MPL_ASSERT((
 boost::mpl::equal<
  vector<vector<int_<1> > >,
  list<list<int_<1> > >,
  boost::mpl::lambda<
   boost::mpl::equal<
    boost::mpl::_1,
    boost::mpl::_2,
    boost::mpl::lambda<
     boost::is_same<
      boost::mpl::_1,
      boost::mpl::_2
     >
    >::type
   >
  >::type
 >));

やってることは同じである。というか bind base の方は実質 lambda を展開した結果になっている。二次元でこれだとそれ以上だと絶望しそうだ。ということで再帰的な equal を書いてみた。

template<typename Arg1, typename Arg2, typename Pred = boost::is_same<boost::mpl::_1, boost::mpl::_2> >
struct equal_deep
{
 typedef typename boost::mpl::eval_if<
  boost::mpl::and_<boost::mpl::is_sequence<Arg1>, boost::mpl::is_sequence<Arg2> >,
  boost::mpl::equal<Arg1, Arg2, equal_deep<boost::mpl::_1, boost::mpl::_2, typename boost::mpl::lambda<Pred>::type> >,
  boost::mpl::eval_if<
   boost::mpl::or_<boost::mpl::is_sequence<Arg1>, boost::mpl::is_sequence<Arg2> >,
   boost::mpl::false_,
   boost::mpl::apply<Pred, Arg1, Arg2>
  >
 >::type type;
};
BOOST_MPL_ASSERT((
 equal_deep<
  vector<vector<int_<1> > >,
  list<list<int_<1> > >,
 >));

やっていることはそれほど難しくない。引数が両方 Sequence である時には equal に対して自分自身(equal_deep)を渡して再帰し、両方が Sequence でない時には渡された Predicate を呼び出す。自分が MPL Metafunction 関係をちゃんと把握していなかったので lambda かけるところと apply で迷ったぐらいである。7 行目の eval_if は if_ だと is_same の場合は大丈夫だが Sequence に対して呼び出せない Predicate (equal_to 等) を渡した場合にコンパイルエラーになるため eval_if でなければならない。また、nullary Metafunction を受け付ける部分は typename ::type を付けずに不要なインスタンス化を避けている。

これを書く際にせっかくなので一度 MPL Metafunction 系について整理してみたのだが以下次号。