2016-12-07

参考書に昔の技法を書くべきか:C++17のコンパイル時分岐

今、C++17のライブラリの参考書を書いているのだが、C++14時代の、今は現役だが、もうすぐ古代の技術になる技法を紹介すべきかどうか迷っている。

問題はコンパイル時分岐だ。たとえば、イテレーターがランダムアクセスイテレーターかどうかで、最適な処理が異なるアルゴリズムがあったとする。以下のように書けばいいだろうか。

template < typename Iterator >
void algorithm( Iterator first, Iterator last )
{
    if ( std::is_same<
            std::random_access_iterator_tag,
            typename std::iterator_traits<Iterator>::iterator_category
        >{}
    )
    {
        // ランダムアクセスイテレーターに特化した高速なアルゴリズム
        first + 1 ; // ランダムアクセスイテレーターの処理の例
    }
    else {
    // Forward Iteratorにも対応できるアルゴリズム
    }
}

残念ながら、このコードにランダムアクセスイテレーター以外を渡すとコンパイルエラーになる。その理由は、イテレーターと整数をoperaotr +に渡しているからだ。これはランダムアクセスイテレーターしか提供していない操作だ。

コンパイルエラーを防ぐには、あるテンプレートコードが条件次第で実体化される措置が必要だ。つまり、コンパイル時分岐が必要になる。

C++14でコンパイル時分岐を実現する方法はふたつある。関数テンプレートのオーバーロードを使う方法と、テンプレートの特殊化(部分的特殊化)だ。

関数テンプレートのオーバーロードを使うには、以下のようにiterator_tagでオーバーロード解決を行う。


template < typename Iterator >
void algorithm_impl( Iterator first, Iterator last,
    std::random_access_iterator_tag )
{
// ランダムアクセスイテレーターを必要とする処理
}

template < typename Iterator >
void algorithm_impl( Iterator first, Iterator last,
    std::bidirectional_iterator_tag )
{
// 双方向イテレーターを必要とする処理
}

template < typename Iterator >
void algorithm( Iterator first, Iterator last )
{

    algorithm_impl( first, last,
        typename std::iterator_traits<Iterator>::iterator_category{}
    ) ;
}

テンプレートの特殊化は特にひねりはない。

template < typename T >
struct algorithm_impl
{
template < typename Iterator >
static void process( Iterator first, Iterator last )
{
// 前方イテレーター以上が必要な処理
}

} ;

template  <>
struct algorithm_impl< std::random_access_iterator_tag >
{
template < typename Iterator >
static void process( Iterator first, Iterator last )
{
    first + 1 ;
}

}

template < typename Iterator >
void algorithm( Iterator first, Iterator last )
{

    algorithm_impl<
        typename std::iterator_traits<Iterator>::iterator_category
    >::process( first, last ) ;
}

このようにコンパイル時分岐は実現できるのだが、C++17ではconstexpr ifが入ったことでこのような技法は古臭いハックに成り下がってしまった。

template < typename Iterator >
void algorithm( Iterator first, Iterator last )
{
    using iterator_category = typename std::iterator_tratis<Iterator>::iterator_category ;

    // ランダムアクセスイテレーターの場合の処理
    if constexpr ( std::is_same< iterator_category, std::random_access_iterator_tag >{} )
    {
        first + 1 ;
    }
    // 前方イテレーター以上
    else
    {
    }
}

constexpr ifがあれば、昔の泥臭いコンパイル時分岐ハックはいらなくなる。とすれば、参考書にわざわざ昔のハックを書く必要はない。

とはいえ、それはC++17が普及してからの話だ。C++17が制定されるのにまだ1年かかり、GCCやClangの規格準拠のC++コンパイラーの安定版がリリースされるまでに数年かかり、普及には更に時間がかかる。

とはいえ、歴史を振り返れば、かつてのenumハックがstatic constな整数型のデータメンバーになり、今ではstatic constexprなデータメンバーになっているのを考えると、わざわざ昔のハックを載せる必要はないように思える。

ドワンゴ広告

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

1 comment:

Anonymous said...

メタプログラミングの技法、知らない人は全然知らないし(注: 自分も全然知らない)、
そういう人が、既存のプログラムを読んだときに、これは古い技法だと気づくことが
できるので、古いやり方と新しいやり方を併記するのは有用だと思います。