web-dev-qa-db-ja.com

C ++ 17の新しい範囲ベースのforループはRanges TSにどのように役立ちますか?

委員会は、範囲ベースのforループを以下から変更しました。

  • C++ 11:

    {
       auto && __range = range_expression ; 
       for (auto __begin = begin_expr, __end = end_expr; 
           __begin != __end; ++__begin) { 
           range_declaration = *__begin; 
           loop_statement 
       }
    } 
    
  • c ++ 17へ:

    {        
        auto && __range = range_expression ; 
        auto __begin = begin_expr ;
        auto __end = end_expr ;
        for ( ; __begin != __end; ++__begin) { 
            range_declaration = *__begin; 
            loop_statement 
        } 
    }
    

そして人々は、これによりRanges TSの実装が容易になると言った。例を挙げていただけますか?

63
Dimitar Mirchev

C++ 11/14の範囲-forが過剰に制約されていました...

このためのWG21論文は P0184R0 であり、これには以下の動機があります。

既存の範囲ベースのforループには過剰な制約があります。終了反復子は、インクリメント、デクリメント、または間接参照されることはありません。イテレータにすることを要求することは、実際的な目的には役立ちません。

投稿したStandardeseからわかるように、範囲のendイテレータは、ループ条件__begin != __end;でのみ使用されます。したがって、endbeginと同等の等値である必要があり、逆参照可能または増分可能である必要はありません。

...区切りイテレータのoperator==を歪めます。

では、これにはどのような欠点がありますか?センチネルで区切られた範囲(C文字列、テキスト行など)がある場合は、ループ条件を反復子のoperator==にシューホーンする必要があります。

#include <iostream>

template <char Delim = 0>
struct StringIterator
{
    char const* ptr = nullptr;   

    friend auto operator==(StringIterator lhs, StringIterator rhs) {
        return lhs.ptr ? (rhs.ptr || (*lhs.ptr == Delim)) : (!rhs.ptr || (*rhs.ptr == Delim));
    }

    friend auto operator!=(StringIterator lhs, StringIterator rhs) {
        return !(lhs == rhs);
    }

    auto& operator*()  {        return *ptr;  }
    auto& operator++() { ++ptr; return *this; }
};

template <char Delim = 0>
class StringRange
{
    StringIterator<Delim> it;
public:
    StringRange(char const* ptr) : it{ptr} {}
    auto begin() { return it;                      }
    auto end()   { return StringIterator<Delim>{}; }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : StringRange<'!'>{"Hello World!"})
        std::cout << c;
}

Live Example g ++ -std = c ++ 14、( Assembly gcc.godbolt.orgを使用)

上記のoperator==StringIterator<>は引数が対称であり、range-forがbegin != endであるかend != beginであるかに依存しません(そうでなければ、コードをチートしてカットできます)ハーフ)。

単純な反復パターンの場合、コンパイラはoperator==内の複雑なロジックを最適化できます。実際、上記の例では、operator==は単一の比較に削減されます。しかし、これは範囲とフィルターの長いパイプラインで引き続き機能しますか?知るか。英雄的な最適化レベルが必要になる可能性があります。

C++ 17は制約を緩和し、区切られた範囲を単純化します...

それでは、単純化はどこで正確に現れますか? operator==では、イテレータ/センチネルペア(対称性のために両方の順序で)を取る余分なオーバーロードがあります。したがって、実行時ロジックはコンパイル時ロジックになります。

#include <iostream>

template <char Delim = 0>
struct StringSentinel {};

struct StringIterator
{
    char const* ptr = nullptr;   

    template <char Delim>
    friend auto operator==(StringIterator lhs, StringSentinel<Delim> rhs) {
        return *lhs.ptr == Delim;
    }

    template <char Delim>
    friend auto operator==(StringSentinel<Delim> lhs, StringIterator rhs) {
        return rhs == lhs;
    }

    template <char Delim>
    friend auto operator!=(StringIterator lhs, StringSentinel<Delim> rhs) {
        return !(lhs == rhs);
    }

    template <char Delim>
    friend auto operator!=(StringSentinel<Delim> lhs, StringIterator rhs) {
        return !(lhs == rhs);
    }

    auto& operator*()  {        return *ptr;  }
    auto& operator++() { ++ptr; return *this; }
};

template <char Delim = 0>
class StringRange
{
    StringIterator it;
public:
    StringRange(char const* ptr) : it{ptr} {}
    auto begin() { return it;                      }
    auto end()   { return StringSentinel<Delim>{}; }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : StringRange<'!'>{"Hello World!"})
        std::cout << c;
}

Live Example g ++ -std = c ++ 1zを使用( Assembly gcc.godbolt.orgを使用します。これは前の例とほとんど同じです)。

...実際、完全に一般的なプリミティブな「Dスタイル」範囲をサポートします。

WG21ペーパー N4382 には次の提案があります:

C.6 Range Facade and Adapter Utilities [future.facade]

1ユーザーが独自のイテレータータイプを作成するのが簡単になるまで、イテレーターの可能性は未実現のままです。範囲の抽象化により、それが実現可能になります。適切なライブラリコンポーネントを使用すると、ユーザーは最小限のインターフェイス(たとえば、currentdone、およびnextメンバー)で範囲を定義し、イテレーターを使用できるようになります。自動的に生成されるタイプ。このような範囲ファサードクラステンプレートは、今後の作業として残されます。

基本的に、これはDスタイルの範囲に相当します(これらのプリミティブはemptyfrontおよびpopFrontと呼ばれます)。これらのプリミティブのみで区切られた文字列範囲は、次のようになります。

template <char Delim = 0>
class PrimitiveStringRange
{
    char const* ptr;
public:    
    PrimitiveStringRange(char const* c) : ptr{c} {}
    auto& current()    { return *ptr;          }
    auto  done() const { return *ptr == Delim; }
    auto  next()       { ++ptr;                }
};

プリミティブ範囲の基礎となる表現がわからない場合、それからイテレーターを抽出する方法は? range -forで使用できる範囲にこれを適応させる方法は? 1つの方法(@ -EricNieblerによる 一連のブログ投稿 も参照)と@ T.C。からのコメント:

#include <iostream>

// adapt any primitive range with current/done/next to Iterator/Sentinel pair with begin/end
template <class Derived>
struct RangeAdaptor : private Derived
{      
    using Derived::Derived;

    struct Sentinel {};

    struct Iterator
    {
        Derived*  rng;

        friend auto operator==(Iterator it, Sentinel) { return it.rng->done(); }
        friend auto operator==(Sentinel, Iterator it) { return it.rng->done(); }

        friend auto operator!=(Iterator lhs, Sentinel rhs) { return !(lhs == rhs); }
        friend auto operator!=(Sentinel lhs, Iterator rhs) { return !(lhs == rhs); }

        auto& operator*()  {              return rng->current(); }
        auto& operator++() { rng->next(); return *this;          }
    };

    auto begin() { return Iterator{this}; }
    auto end()   { return Sentinel{};     }
};

int main()
{
    // "Hello World", no exclamation mark
    for (auto const& c : RangeAdaptor<PrimitiveStringRange<'!'>>{"Hello World!"})
        std::cout << c;
}

Live Example g ++ -std = c ++ 1zを使用( Assembly gcc.godbolt.orgを使用)

結論:センチネルはデリミタを型システムに押し込むだけのかわいいメカニズムではなく、 プリミティブ「Dスタイル」範囲 (それ自体はイテレータの概念を持たない場合があります)を新しいC++ 1z範囲のゼロオーバーヘッド抽象化としてサポートします。

47
TemplateRex

新しい仕様では、__begin__endと比較して不等式である限り、__end__beginを異なるタイプにすることができます。 __endはイテレータである必要はなく、述語にすることもできます。 beginendのメンバーを定義する構造体を持つ愚かな例です。後者は反復子ではなく述語です:

#include <iostream>
#include <string>

// a struct to get the first Word of a string

struct FirstWord {
    std::string data;

    // declare a predicate to make ' ' a string ender

    struct EndOfString {
        bool operator()(std::string::iterator it) { return (*it) != '\0' && (*it) != ' '; }
    };

    std::string::iterator begin() { return data.begin(); }
    EndOfString end() { return EndOfString(); }
};

// declare the comparison operator

bool operator!=(std::string::iterator it, FirstWord::EndOfString p) { return p(it); }

// test

int main() {
    for (auto c : {"Hello World !!!"})
        std::cout << c;
    std::cout << std::endl; // print "Hello World !!!"

    for (auto c : FirstWord{"Hello World !!!"}) // works with gcc with C++17 enabled
        std::cout << c;
    std::cout << std::endl; // print "Hello"
}
38
wasthishelpful