web-dev-qa-db-fra.com

Comment la nouvelle boucle for basée sur une plage en C ++ 17 aide Ranges TS?

Le comité a modifié la boucle basée sur la plage pour:

  • C++ 11:

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

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

Et les gens ont dit que cela faciliterait la mise en œuvre de Ranges TS. Pouvez-vous me donner quelques exemples?

63
Dimitar Mirchev

La plage C++ 11/14 -for était surcontrainte ...

Le document du WG21 pour cela est P0184R0 qui a la motivation suivante:

La boucle for basée sur une plage existante est trop contrainte. L'itérateur de fin n'est jamais incrémenté, décrémenté ou déréférencé. Exiger qu'il soit un itérateur ne sert à rien.

Comme vous pouvez le voir dans le Standardese que vous avez publié, l'itérateur end d'une plage n'est utilisé que dans la condition de boucle __begin != __end;. Par conséquent, end doit uniquement être égal à comparable à begin, et il n'a pas besoin d'être déréférencable ou incrémentable.

... ce qui fausse operator== pour les itérateurs délimités.

Quel est donc cet inconvénient? Eh bien, si vous avez une plage délimitée par des sentinelles (chaîne en C, ligne de texte, etc.), vous devez chausse-pied la condition de boucle dans le operator==, essentiellement comme ceci

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

Exemple Live avec g ++ -std = c ++ 14, ( Assembly en utilisant gcc.godbolt.org)

Ce qui précède operator== pour StringIterator<> est symétrique dans ses arguments et ne dépend pas du fait que la fourchette soit begin != end ou end != begin (sinon vous pourriez tricher et couper le code en deux).

Pour les modèles d'itération simples, le compilateur est en mesure d'optimiser la logique alambiquée à l'intérieur de operator==. En effet, pour l'exemple ci-dessus, le operator== est réduit à une seule comparaison. Mais cela continuera-t-il de fonctionner pour de longs pipelines de gammes et de filtres? Qui sait. Il est susceptible de nécessiter des niveaux d'optimisation héroïques.

C++ 17 assouplira les contraintes qui simplifieront les plages délimitées ...

Alors, où se manifeste exactement la simplification? Dans operator==, qui a maintenant des surcharges supplémentaires prenant une paire itérateur/sentinelle (dans les deux ordres, par symétrie). Ainsi, la logique d'exécution devient une logique de compilation.

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

Exemple Live utilisant g ++ -std = c ++ 1z ( Assembly en utilisant gcc.godbolt.org, qui est presque identique à l'exemple précédent).

... et supportera en fait des gammes entièrement générales et primitives de "style D".

Le papier WG21 N4382 a la suggestion suivante:

C.6 Utilitaires de façade et d'adaptateur de plage [future.facade]

1 Jusqu'à ce qu'il devienne trivial pour les utilisateurs de créer leurs propres types d'itérateurs, le plein potentiel des itérateurs restera non réalisé. L'abstraction de la plage rend cela réalisable. Avec les bons composants de bibliothèque, les utilisateurs devraient pouvoir définir une plage avec une interface minimale (par exemple, les membres current, done et next), et disposer d'un itérateur types générés automatiquement. Un tel modèle de classe de façade de plage est laissé comme travail futur.

Essentiellement, cela est égal aux plages de style D (où ces primitives sont appelées empty, front et popFront). Une plage de chaînes délimitée avec uniquement ces primitives ressemblerait à ceci:

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

Si l'on ne connaît pas la représentation sous-jacente d'une plage primitive, comment en extraire des itérateurs? Comment l'adapter à une plage utilisable avec la plage -for? Voici une façon (voir aussi la série de billets de blog par @EricNiebler) et les commentaires de @ 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;
}

Exemple Live utilisant g ++ -std = c ++ 1z ( Assembly en utilisant gcc.godbolt.org)

Conclusion : les sentinelles ne sont pas seulement un joli mécanisme pour insérer des délimiteurs dans le système de type, elles sont assez générales pour supporter la primitive "D- style "gammes (qui elles-mêmes n'ont peut-être aucune notion d'itérateurs) comme abstraction zéro pour le nouveau range-for C++ 1z.

47
TemplateRex

La nouvelle spécification permet __begin et __end être de type différent, tant que __end peut être comparé à __begin pour l'inégalité. __end n'a même pas besoin d'être un itérateur et peut être un prédicat. Voici un exemple stupide avec une structure définissant les membres begin et end, ce dernier étant un prédicat au lieu d'un itérateur:

#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