wake-up-neo.com

Wie hilft die neue bereichsbasierte for-Schleife in C ++ 17 Ranges TS?

Das Komitee änderte die bereichsbezogene for-Schleife von:

  • C++ 11:

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

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

Und die Leute sagten, dass dies die Implementierung von Ranges TS erleichtern wird. Können Sie mir einige Beispiele nennen?

63
Dimitar Mirchev

C++ 11/14-Bereich -for war überfordert ...

Das WG21-Papier hierfür ist P0184R0 mit der folgenden Motivation:

Die vorhandene bereichsbasierte for-Schleife ist überfordert. Der End-Iterator wird niemals inkrementiert, dekrementiert oder dereferenziert. Das Erfordernis, ein Iterator zu sein, hat keinen praktischen Zweck.

Wie Sie den von Ihnen geposteten Standardesen entnehmen können, wird der Iterator end eines Bereichs nur in der Schleifenbedingung __begin != __end; Verwendet. Daher muss end nur mit begin vergleichbar sein, und es muss nicht dereferenzierbar oder inkrementierbar sein.

... was operator== für durch Trennzeichen getrennte Iteratoren verzerrt.

Welchen Nachteil hat das? Nun, wenn Sie einen durch Sentinels getrennten Bereich haben (C-String, Textzeile usw.), müssen Sie die Schleifenbedingung in den Iterator operator== Einfügen, im Wesentlichen so

#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-Beispiel mit g ++ -std = c ++ 14, ( Assembly mit gcc.godbolt.org)

Das obige operator== Für StringIterator<> Ist in seinen Argumenten symmetrisch und hängt nicht davon ab, ob der Gültigkeitsbereich begin != end Oder end != begin Ist (ansonsten könnten Sie betrügen und halbieren Sie den Code).

Für einfache Iterationsmuster kann der Compiler die gewundene Logik in operator== Optimieren. Tatsächlich wird für das obige Beispiel operator== Auf einen einzigen Vergleich reduziert. Aber wird dies auch weiterhin für lange Pipelines von Bereichen und Filtern funktionieren? Wer weiß. Es wird wahrscheinlich heldenhafte Optimierungsstufen erfordern.

Mit C++ 17 werden die Einschränkungen gelockert, wodurch begrenzte Bereiche vereinfacht werden ...

Wo genau zeigt sich die Vereinfachung? In operator==, Das jetzt zusätzliche Überladungen hat, die ein Iterator/Sentinel-Paar benötigen (in beiden Reihenfolgen aus Symmetriegründen). So wird die Laufzeitlogik zur Kompilierzeitlogik.

#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-Beispiel mit g ++ -std = c ++ 1z ( Assembly mit gcc.godbolt.org, was fast identisch mit dem vorherigen Beispiel ist).

... und wird in der Tat allgemeine, primitive "D-style" -Reihen unterstützen.

WG21-Papier N4382 hat den folgenden Vorschlag:

C.6 Dienstprogramme für Range Facade und Adapter [future.facade]

1 Bis es für Benutzer trivial ist, eigene Iteratortypen zu erstellen, wird das volle Potenzial von Iteratoren nicht ausgeschöpft. Die Sortimentsabstraktion macht das erreichbar. Mit den richtigen Bibliothekskomponenten sollte es Benutzern möglich sein, einen Bereich mit einer minimalen Schnittstelle zu definieren (z. B. current, done und next Mitglieder) und über einen Iterator zu verfügen Typen automatisch generiert. Eine solche Range-Facade-Klassenvorlage bleibt als zukünftige Arbeit übrig.

Dies entspricht im Wesentlichen D-Bereichen (wobei diese Grundelemente als empty, front und popFront bezeichnet werden). Ein abgegrenzter Zeichenfolgenbereich mit nur diesen Grundelementen würde ungefähr so ​​aussehen:

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;                }
};

Wenn man die zugrunde liegende Darstellung eines primitiven Bereichs nicht kennt, wie kann man Iteratoren daraus extrahieren? Wie passt man dies an einen Bereich an, der mit range -for verwendet werden kann? Hier ist eine Möglichkeit (siehe auch die Reihe von Blog-Beiträgen von @EricNiebler) und die Kommentare von @ TC:

#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-Beispiel mit g ++ -std = c ++ 1z ( Assembly mit gcc.godbolt.org)

Fazit : Sentinels sind nicht nur ein netter Mechanismus, um Trennzeichen in das Typsystem einzufügen, sie sind allgemein genug, um unterstützt primitive "D-style" Bereiche (die selbst keine Vorstellung von Iteratoren haben können) als Null-Overhead-Abstraktion für das neue C++ 1z range-for.

47
TemplateRex

Die neue Spezifikation erlaubt, dass __begin Und __end Unterschiedliche Typen haben, solange __end Mit __begin Auf Ungleichheit verglichen werden kann. __end Muss nicht einmal ein Iterator sein und kann ein Prädikat sein. Hier ist ein albernes Beispiel mit einer Struktur, die begin und end Elemente definiert, wobei letzteres ein Prädikat anstelle eines Iterators ist:

#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