2017-10-23

インデックス付きrange-based forに必要なのはネストされた構造化束縛ではなくてif constexprだった

親愛なるC++読者諸君、私がC++17を流暢に使いこなす江添亮である。前回、ネストされた構造化束縛がほしいと書いた。

本の虫: インデックス付きRange-based for文を実装したらネストされた構造化束縛が欲しくなった

このブログを公開してすぐ、同僚からwith_index側でpairを開けばいいのではないかと言われた。たしかにそのとおりだ。早速実装した。その結果、以下のコードが通るようになった。


int main()
{
    std::map<int,int> m = { {1,1}, {2,2},{3,3} } ;

    for ( auto[ i, key, mapped ] : with_index(m) )
    {
        std::cout << i << key << mapped ;
    }
}

その実装の骨子は以下の通り。


template < typename T >
struct is_pair
    : std::false_type { } ;

template < typename T1, typename T2 >
struct is_pair< std::pair<T1, T2> >
    : std::true_type { } ;

template < typename T >
constexpr bool is_pair_v = is_pair<T>::value ;

template < typename Iterator  >
class with_index_iterator
    : public Iterator
{

public :
    auto operator *() const noexcept
    {   // ここが重要
        if constexpr ( is_pair_v< typename std::iterator_traits<Iterator>::value_type > )
        {
            auto & pair = *static_cast<Iterator const &>(*this) ;
            return std::make_tuple( i, pair.first, pair.second ) ;
        }
        else {
            return std::make_pair( i, *static_cast<Iterator const &>(*this) ) ;
        }
    }
} ;

重要なのはif constexprだ。constexpr ifによって、わざわざクラステンプレートを書かずともコンパイル時条件分岐ができるようになった。これでメタプログラミングがとても書きやすくなった。

なお、この実装では以下のようなコードは通らない。

int main()
{
    std::map<int, std::pair<int,int> > m = { {1,{1,1}}, {2,{2,2}},{3,{3,} } ;

    for ( auto[ i, key, m1, m2 ] : with_index(m) )
    { }
}

再帰的なテンプレートメタプログラミングをすることにより、何段階にネストされようとも開くことができるwith_indexは実装可能だ。その実装は読者への課題とする。

また、構造化束縛がpairの他にも対応しているtupleやtuple_sizeとtuple_elementとgetに対応した型のネストへの対応も、読者への課題とする。

int main()
{
    std::map<int, std::tuple<int,int> > m = { {1,{1,1}}, {2,{2,2}},{3,{3,} } ;

    for ( auto[ i, key, m1, m2 ] : with_index(m) )
    { }
}

問題は、構造化束縛が対応しているクラスを開くことができない。


struct user_defined_pair
{
    int x ;
    int y ;
} ;

std::map< int, user_defined_pair > m = { {1,{1,1}}, {2,{2,2}},{3,{3,} } ;

これはどうしようもない。とはいえ、実用上はこれでいいのではないか。

with_indexの完全な実装は以下の通り。

template < typename T >
struct is_pair
    : std::false_type { } ;

template < typename T1, typename T2 >
struct is_pair< std::pair<T1, T2> >
    : std::true_type { } ;

template < typename T >
constexpr bool is_pair_v = is_pair<T>::value ;



template < typename Iterator  >
class with_index_iterator
    : public Iterator
{
    std::size_t i = 0 ;

public :

    with_index_iterator( Iterator iter )
        : Iterator( iter )
    { }

    auto & operator ++()
    {
        ++i ;
        this->Iterator::operator ++() ;
        return *this ;
    }

    auto operator *() const noexcept
    {
        if constexpr ( is_pair_v< typename std::iterator_traits<Iterator>::value_type > )
        {
            auto & pair = *static_cast<Iterator const &>(*this) ;
            return std::make_tuple( i, pair.first, pair.second ) ;
        }
        else {
            return std::make_pair( i, *static_cast<Iterator const &>(*this) ) ;
        }
    }

} ;

template < typename Range >
class with_index
{
    Range & range ;

public :
    with_index( Range & range )
        : range(range)
    { }

    auto begin() const
    {
        return with_index_iterator{ std::begin(range) } ;
    }
    auto end() const
    {
        return with_index_iterator{ std::end(range) } ;
    }

} ;

ドワンゴ広告

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

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

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

4 comments:

Anonymous said...

この実装では、std::vector>に対して[index, element]で受けることができず、必ず[index, first, second]を使わねばならない、という制約があるので、必ずしも適切ではないように思えます。
もちろん、この条件をstd::map(や、std::unordered_mapなど)のみに限定することは可能でですが、そうすると今度はstd::vector>に対する[index, first, second]が使えなくなってしまう。
ですので、if constexprで分岐するよりも、[index, element]版と展開版を、別関数(あるいは別オーバーロード)として用意するのがより良いように思えます。
あるいは、やはりネストされた構造化束縛を使うか。

Anonymous said...

しまった、<>がタグ扱いで消えた……。std::vector<std::pair<T, U>>

Anonymous said...

mapをindex付きで取り出したい事なんて殆どないから元のやつで十分幸せになれる気がする。

Anonymous said...

読者への課題はすべて著者への課題にしてくれ