読者です 読者をやめる 読者になる 読者になる

range-based for が気になる。

C++

■ 概要
 C++0x の range-based for について議論が行われているらしい。↓この記事にある全 5 案のどれが良いのか...。
『range-based forに対する意見求む』 本の虫

 ざっとまとめるとこんな感じ。


・案1 ADL のみ
・案2 range_traits の特殊化 のみ
・案3 range_traits の特殊化 + ADL
・案4 メンバ関数 begin, end のみ
・案5 メンバ関数 begin, end + ADL

Twitter 上での議論は↓ここ。
『range-based for の変更案』 Togetter


で、悩んだ末、↓こうなりました。

■ range-based for を使いたい。
 range-based for が言語仕様として取り入れられることは嬉しいけど、もし「定義を変更できないクラスは通常の for ループを書くしか無い」というのであればそれでは片手落ちになる気がします。なので何とかして外部から range-based for に適合させるしくみがあるべきだと思うのです。

■ アダプタ利用の是非。
 まずアダプタについて想像してみます。もし仮に選択肢がアダプタのみという仕様であるなら、それはメタプログラミングの複雑さを招くような気がします。

 例えばこういうコード。

template < typename T1 > void foo( T1 & t1 ) {
    for( auto i: t1 ) { cout << i; }
}
template < typename T2 > void bar( T2 & t2 ) {
    for( auto i: t2 ) { foo( i ); }
}
int main()
{
    vector< vector< int > > v1 = ???;
    bar( v1 );
    
    vector< my_vector< int > > v2 = ???;
    bar( v2 ); // ※
}

 ここで my_vector に begin(), end() が無いと ※ のところでコンパイルエラーになります。それは嫌なのでどうするかっていうと foo< my_vector > という特殊化を定義してやればよいということになります。

template < typename T > void foo< my_vector< T > >( my_vector< T > & t2 )
{
    for( auto i: my_adapter( t2 ) { cout << i; }
}

 これは苦痛じゃないですかね。

 定義を変更できない型があったとして、それを使って range-based for したいとき、range-based for を使った数だけ、range-based for を使っているコードの特殊化を書かないといけない。*1
 しかし仮に range_traits を利用するなら 一度だけ、range_traits の特殊化を書けば済むようになります。
 だから 案1 と 案4 はイマイチな感じがするのです。

■ range_traits の特殊化の是非。
 ところで「range_traits の特殊化は初心者には難しい」という指摘があります。これは全くその通りだと思います。が、案 3 には ADL という逃げ道があります。冒頭の 記事でも紹介されていますが、begin, end 関数を同じ名前空間に定義してやるだけで range-based for に対応させられます。template の知識すら要せず、元の型を変更せずに可能になるわけです。これが 案 2 に対する 案 3 のアドバンテージです。
 あと自作クラスであればメンバ関数 begin, end を定義してやれば range_traits がデフォルトでそれらを呼ぶので明示的に特殊化を書く必要もありません。(thx! @melponn)
 あぁあと、もちろん range_traits の特殊化によって定義を上書きされてしまうという危険が残りますが、それは上級者による意図的なハックなので問題視していません。

■ ADL をアテにすることの是非。
 また一方で「ADL は邪悪。」という意見があります。これも全くその通りだと思います。しかし ADL がなぜ邪悪かというと、意図しない関数が名前でマッチしてしまって、意図しないコードが生成されるから邪悪なのです。僕が ADL があるにも関わらず案 3 が良いと思う理由は、range-based for においては begin, end という名前が既に決まっているわけなので危険は限定的だと思うからです。
 案 3 の range-based for で ADL の被害を受けるのは例えば以下の条件を全て満たすときです。


・range_traits の特殊化が間違っている。(意図的なハックがミスを含んでいる)
・begin, end という名前で引数に T & をとるような関数が ADL に捕まる範囲に定義されている。
 or
 T にメンバ関数 begin, end がある。
・range-based for に T のオブジェクトを渡している。
ほかにもこんなの。

・T の基底クラスが range_traits の明示的特殊化の対象になっている。
 or
 明示的な ADL 利用によってのみ range-based for の対象になっている。
 (しかし継承禁止の指定が無い)
・T または T の基底クラスに begin, end がある。
・T の range_traits の明示的特殊化が無い。
・range-based for に T のオブジェクトを渡している。
 ADL 利用の是非の要は、このリスクを飲むのかそれとも毎回アダプタを書くか、どっちがマシかということです。僕はこの程度のリスクなら飲んでも良いと思います。

あと、ちょっとテーマからずれるけど...
■ カルチャー
 冒頭の blog でもちらっと書かれていますが、いまのうちからアダプタを書かせておけばコンセプトの導入にアドバンテージができます。これは大きなメリットでしょう。しかし仮にコンセプト未搭載の C++0x で実際にアダプタを利用するとなると、その型名、保持するコンテナオブジェクトの変数名はそれぞれどう書きましょうか。何らかの指針が無いと、俺々 adapter_base< T > が量産されそうです。それは望ましくない。恐らく、コンセプト導入を見越したアダプタの書き方が奨励されることでしょ。

 ところでこの range-based for について考えてて、ひとつ疑問が。

例えば、int の2次元行列クラス matrix_t があったとして、あるときは要素全てで、あるときは行で、あるときは列でループさせたい、とします。range-based for をうまく使えないでしょうか。

・matrix_t の begin(), end() は要素全てのイテレーションを提供する。
  for( int i: mat ) { ... }
・行のイテレーションをするためのアダプタクラス matrix_rows が提供される。
  for( row_t r: matrix_rows( mat ) ) { ... }
・行のイテレーションをするためのアダプタクラス matrix_cols が提供される。
  for( col_t c: matrix_cols( mat ) ) { ... }
 アダプタを数種類提供することで、複数のループ方法を提供することができるようになります。デフォルトではこういうループをするけど、他の方法も選択できるよ、と matrix_t 自身が明示できるような、カルチャーと言うと大げさなカルチャーが出来上がるんじゃなかろーか、という妄想です。はい。

*1:あと、メタなプログラムを書かない初心者ユーザーにとってもいちいち書くのはやっぱり面倒臭いです