66 votes

Comment la nouvelle boucle for basée sur les intervalles dans C++17 aide Ranges TS ?

Le comité a modifié la fourchette de la boucle pour :

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

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

51voto

TemplateRex Points 26447

Gamme C++11/14- for était surcontrainte...

Le document du WG21 à ce sujet est le suivant P0184R0 qui a la motivation suivante :

La boucle for existante, basée sur l'intervalle, est surcontrainte. L'itérateur n'est jamais incrémenté, décrémenté ou déréférencé. En exigeant que qu'il soit un itérateur n'a aucun intérêt pratique.

Comme vous pouvez le voir dans le Standardese que vous avez posté, la end l'itérateur d'une plage n'est utilisé que dans la condition de boucle __begin != __end; . Par conséquent, end Il suffit que l'égalité soit comparable à begin et il n'est pas nécessaire qu'il soit déréférençable ou incrémentable.

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

Alors, quel inconvénient cela présente-t-il ? Eh bien, si vous avez une plage délimitée par une sentinelle (chaîne de caractères C, ligne de texte, etc.), vous devez alors insérer la condition de boucle dans l'attribut 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 en direct avec g++ -std=c++14, ( montage 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 gamme-pour soit begin != end ou end != begin (sinon vous pourriez tricher et couper le code en deux).

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

C++17 va assouplir les contraintes, ce qui simplifiera les plages délimitées...

Alors, où se manifeste exactement la simplification ? Sur operator== qui a maintenant des surcharges supplémentaires prenant une paire itérateur/sentinelle (dans les deux ordres, pour la 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 en direct en utilisant g++ -std=c++1z ( montage 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 type "D".

Document du WG21 N4382 a la suggestion suivante :

C.6 Utilités de la façade de la gamme et de l'adaptateur [future.facade].

1 Jusqu'à ce qu'il utilisateurs de créer leurs propres types d'itérateurs, le plein potentiel des itérateurs ne sera pas potentiel des itérateurs restera inexploité. L'abstraction de la plage rend cela possible. Avec les bons composants de bibliothèque, il devrait être possible pour les utilisateurs de définir une plage avec une interface minimale (par ex, current , done y next ), et ont des types d'itérateurs générés automatiquement. Un tel modèle de classe de façade de plage est laissé en tant que travail futur.

Essentiellement, cela équivaut aux plages de style D (où ces primitives sont appelées empty , front y popFront ). Une plage de chaînes de caractères délimitées ne comportant que 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 adapter cela à une plage qui peut être utilisée avec range- for ? Voici un moyen (voir aussi le série d'articles 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 en direct en utilisant g++ -std=c++1z ( montage en utilisant gcc.godbolt.org)

Conclusion Les sentinelles ne sont pas seulement un mécanisme mignon pour insérer des délimiteurs dans le système de types, elles sont suffisamment générales pour que l'on puisse les utiliser dans le cadre d'un projet de recherche. support des gammes primitives "style D (qui peuvent eux-mêmes n'avoir aucune notion des itérateurs) comme une abstraction sans frais pour le nouveau range-for C++1z.

39voto

wasthishelpful Points 10726

La nouvelle spécification permet __begin y __end pour ê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 begin y 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"
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X