2010-09-18

range-based forの興味深い使い方を発見した

初期化の部分を執筆中に、ふと、range-based forの面白い使い方を思いついた

template < typename ... Types >
void f( Types ... args )
{
    for ( auto value : { int(args)... } )
    {
        std::cout << value << std::endl ;
    }
}

int main()
{
    f( 1 ) ;
    f( 1, 2, 3 ) ;
    f( 1, 2, 3, 4, 5 ) ;
}

range-based forには、初期化リストを渡せる。初期化リストには、引数パックを使うことができる。ということは、わざわざ再帰的なアルゴリズムを使わずして、Varidic Templatesを使った可変引数をすべてforで回せるのである。int型にキャストしているのは、Variadic Templatesは、それぞれ型が違う可能性があるからである。

ところで、この関数には、ひとつ問題がある。それは、テンプレートパラメーターパックがゼロ個だった場合、実に意味不明なエラーを出すということである。

int main()
{
    f() ; 
}

gccでは、以下のようなエラーがでる。

In function 'void f(Types ...) [with Types = {}]':
   instantiated from here
 error: unable to deduce 'std::initializer_list<_Tp>&&' from '<brace-enclosed initializer list>()'
 error: unable to deduce 'auto' from '<expression error>'

理由は、range-based forに、空の初期化リストを渡せないためである。

for (auto i : { } ) ; // ill-formed

こんなエラーメッセージでは、関数の実装を知らないユーザーは、何が間違っているのかということが理解出来ない。いったいどうすればいいのか。

ゼロ個のparameter packを禁止したい場合なら、方法はいくつでもある。まず、static_assertを使う方法。

template <typename ... Types >
void f( Types ... args )
{
    static_assert( sizeof...(Types) != 0, "zero parameter pack is not allowed" ) ;
    for ( auto value : { int(args)... } ) ;
}

int main() { f() ; }

エラーメッセージは以下の通りである。

In function 'void f(Types ...) [with Types = {}]':
   instantiated from here
 error: static assertion failed: "zero parameter pack is not allowed"
 error: unable to deduce 'std::initializer_list<_Tp>&&' from '<brace-enclosed initializer list>()'
 error: unable to deduce 'auto' from '<expression error>'

相変わらずエラーはでるが、最初にstatic_assertが表示される。

しかし、人間というものは、エラーメッセージを読まない生き物である。あらゆる人種の中でも、最も怠惰な種族であるプログラマが、エラーメッセージなど読むわけがない。ましてや、static_assertでは、basic source characterしか使えない。とすると必然的に英語で欠かなければならず、日本人にやさしくない。もっと分かりやすいエラーにしたい。

引数をひとつ取るという事も考えられる。

template <typename ... Types >
void f( int x, Types ... args )
{
    for ( auto value : { x, int(args)... } ) ;
}

int main() { f( ) ; }

エラーメッセージは、実に分かりやすくなる。

In function 'int main()':
 error: no matching function for call to 'f()'
 note: candidate is: template<class ... Types> void f(int, Types ...)

ただし、そのためだけにわざわざ引数をひとつ、明示的に欠かなければならないのは苦痛だ。

deleted functionを使うという手もある。

template <typename ... Types >
void f( Types ... args )
{
    for ( auto value : { int(args)... } ) ;
}

void f() = delete ;

int main() { f( ) ; }

エラーは実に分かりやすくなる。

In function 'int main()':
 error: use of deleted function 'void f()'

しかし、これらは、ゼロ個のパラメーターパックを禁止してもよい場合の話である。ゼロ個のパラメータパックを使わせたい場合で、もし、ひとつ以上の引数があれば、range-based forを使いたい場合はどうすればいいのか。結局、その場合は、関数のオーバーロードを使って、別々に実装するか、あるいは、テンプレートメタプログラミングを使って、コンパイル時の条件分岐を行うしか方法がない。

range-based forが、空の初期化リストを、特別な場合として受け入れてくれれば、このような苦労はしなくて済むのだが。

追記:

range-based forは空の初期化リストを受け付けるべきではないかということを話したら、もっと簡単な解決方法を提案された。

for ( auto i : std::initializer_list<int>{} ) ;

そうか、こうすればよかったのだ。空の初期化リストは、型が分からないので受け取れないが、初期化リストのオブジェクトならば、受け取れる。

No comments: