現在、range-based forに対して、改良を加えられるチャンスがある。これはまだ議論中であり、次のFDISに入るかどうか分からないが、未来のC++0xユーザー候補が、今知るべきことだと思うので、ここで説明する。意見がほしい。
問題は、あるクラスを、どのようにrange-based forに対応させるかというものである。現行ドラフトを簡単に説明すると、以下のようになっている。
range-based forに渡した式を引数に取り、イテレーターを返す、begin(expr)、end(expr)を、ADLにより探す。
// Cの構造体による既存のコード
struct Container
{
int * ptr ;
std::size_t size ;
} ;
int * begin( Container & c ) { return ptr ; }
int * end( Container & c ) { return ptr + size ; }
// 同様のconst版
これは、同じ名前空間スコープにbegin、endという名前の関数を書けばいいだけなので、初心者にも簡単といえるだろう。また、クラス定義を変更する必要がない。
また、range-based forのADLには、std名前空間が特別にassociated namespaceとして付け加えられる。std名前空間には、クラスのbegin/endという名前のメンバー関数を呼び出す関数があるので、これを利用することでも、クラスをrange-based forに対応させられる。
つまり、クラス内で、イテレーターを返すbegin/endという名前のメンバー関数を定義する。
// 標準ライブラリのコード
namespace std
{
template <class C> auto begin(C& c) -> decltype(c.begin());
template <class C> auto begin(const C& c) -> decltype(c.begin())
template <class C> auto end(C& c) -> decltype(c.end());
template <class C> auto end(const C& c) -> decltype(c.end());
}
// Containerのためのイテレータークラス
// もちろん、ポインターによる実装も可能
class Iterator ;
class Container
{
Iterator begin( ) ;
Iterator end( ) ;
// 同様のconst版
} ;
この対応は、クラス定義が変更できる場合に、使うことができる。しかし、既存のクラス定義は、様々な理由で、自由に変更できない可能性がある。例えば、Cコードの構造体や、外部のライブラリであり、変更できないという状況は、現実のプログラミングでは、大いにありうる。
しかし、多くの中級者以上のC++プログラマーが知るように、ADLは根本的に邪悪である。今、range-based forに対して、最後の変更を行うチャンスがある。
案1は、現状を維持する。変更は行わない。
案2は、traitsベースの実装に変更する。つまり、range-based forにおけるイテレーターの取得を、テンプレートクラスを通じて行うようになる。
namespace std
{
template<class T>
struct range_traits
{
static typename T::iterator begin(T& t) { return t.begin(); }
static typename T::iterator end(T& t) { return t.end(); }
// 同様のconst版など
} ;
}
クラスをrange-based forに対応させるには、range_traitsの特殊化を行う。
// C++0xの新機能、別の名前空間スコープにおけるテンプレートの特殊化
template < >
struct std::range_traits<Container>
{
int * begin( Container & c ) { return c.ptr ; }
int * end( Container & c ) { return c.ptr + c.size ; }
// 同様のconst版など
} ;
しかしこれでは、ユーザーはテンプレートの特殊化、しかも、STLのテンプレートの特殊化(規格上許可されている)という、かなり上級者向けのコードを書かなければならない。
案3は、案2に加えて、ADLフォールバック機能がつくものである。すなわち、range_traitsによるイテレーターの取得ができなければ、現行のADLベースの実装を使うというものである。これは、range_traitsによる優れた実装を提供しながら、ADLによる泥臭い実装も提供するという意味である。
案4は、イテレーターの取得方法には、メンバー関数、begin/endを使うというものである。つまり、traitsによる対応もできないし、クラス外での関数による対応もできない。ただし、クラス定義を変更せずにrange-based forに対応させる方法はある。アダプターである。
// 既存のCの構造体のような使い方をするクラス
struct Container
{
int * ptr ;
std::size_t size ;
} ;
class Adaptor
{
private :
Container & ref ;
public :
explicit Adaptor( Container & ref )
: ref(ref)
{ }
int * begin() const { return ref.ptr ; }
int * end() const { return ref.ptr + ref.size ; }
} ;
int main()
{
Container c = { new int[10], 10 } ;
for ( auto & value : Adaptor(c) )
{
value = 123 ;
}
delete[] c.ptr ;
}
つまり、言語側の機能は最小限にして、ユーザーコードで対処しようというものである。このようなアダプタークラスは簡単に書けるし、パフォーマンス上の問題もない。また、make_adaptorなどといったヘルパー関数のテクニックを使えば、テンプレート化もできる。
案5は、案4に加えて、メンバー関数が見つからなかった場合に、ADLへのフォールバック機能を付け加えるというものである。案2に対する案3と同じである。つまり、ADLによる泥臭い機能は残される。案5は、現状維持とさほど変わらない。ただ、順番が変わっただけである。ADLの前に、メンバー関数を探しにいくようになるのだ。
私は、案4が最適だと考えている。ADLを利用してはならない。ましてや、range-based forは、将来のC++にコンセプトが入った場合、コンセプトを使った実装に書き換えられるはずなのだ。いま、ADLという穴を作るわけにはいかない。これは、range-based forからADLを外せる、最後のチャンスである。今、ADLを許可すると、一生ADLをサポートしなければならない。
ちなみに、案4のアダプターは、完全にユーザーレベル、ライブラリレベルでの対応なので、どの案を選んだとしても、行うことができる。むしろ、アダプターパターンは非常に一般的なので、C++プログラマーならば、アダプターという手法は、関数を書くことのように、空気を吸って吐くことのように、HBの鉛筆をベキッとヘシ折れるように、できて当然である。アダプターパターンにパフォーマンス上の欠点はない。
意見求む。
追記:配列について
配列は、現行ドラフトですでに、特別に処理されるようになっている。この問題は、配列以外の型に対する実装である。どの案を選んでも、配列に対してrange-based forが使えなくなるということはない。