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.