C ++ 11/14 Bereich - for
war überfordert ...
Das WG21-Papier hierfür ist P0184R0, das die folgende Motivation hat:
Die vorhandene bereichsbasierte for-Schleife ist zu stark eingeschränkt. Der Enditerator wird niemals inkrementiert, dekrementiert oder dereferenziert. Das Erfordernis, ein Iterator zu sein, hat keinen praktischen Zweck.
Wie Sie dem von Ihnen veröffentlichten Standardese entnehmen können, wird der end
Iterator eines Bereichs nur in der Schleifenbedingung verwendet __begin != __end;
. Daher muss end
nur die Gleichheit vergleichbar sein mit begin
und es muss nicht dereferenzierbar oder inkrementierbar sein.
... was operator==
für begrenzte Iteratoren verzerrt .
Welchen Nachteil hat das? Nun, wenn Sie einen durch Sentinel getrennten Bereich haben (C-String, Textzeile usw.), müssen Sie die Schleifenbedingung in den Iterator einbinden operator==
, im Wesentlichen wie folgt
#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()
{
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 Bereich für begin != end
oder ist end != begin
(andernfalls könnten Sie den Code betrügen und halbieren).
Für einfache Iterationsmuster kann der Compiler die verschlungene Logik im Inneren optimieren operator==
. In der Tat wird für das obige Beispiel das operator==
auf einen einzigen Vergleich reduziert. Aber funktioniert dies auch weiterhin für lange Pipelines mit Bereichen und Filtern? Wer weiß. Es ist wahrscheinlich, dass heldenhafte Optimierungsstufen erforderlich sind.
C ++ 17 lockert die Einschränkungen, wodurch begrenzte Bereiche vereinfacht werden ...
Wo genau manifestiert sich die Vereinfachung? In operator==
, das jetzt zusätzliche Überladungen aufweist, die ein Iterator / Sentinel-Paar erfordern (in beiden Reihenfolgen für Symmetrie). Die Laufzeitlogik wird also 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()
{
for (auto const& c : StringRange<'!'>{"Hello World!"})
std::cout << c;
}
Live-Beispiel mit g ++ -std = c ++ 1z ( Assembly mit gcc.godbolt.org, die fast identisch mit dem vorherigen Beispiel ist).
... und wird in der Tat ganz allgemeine, primitive "D-Style" -Bereiche unterstützen.
Das WG21-Papier N4382 enthält den folgenden Vorschlag:
C.6 Range Facade- und Adapter-Dienstprogramme [future.facade]
1 Bis es für Benutzer trivial wird, ihre eigenen Iteratortypen zu erstellen, bleibt das volle Potenzial von Iteratoren unrealisiert. Die Bereichsabstraktion macht dies erreichbar. Mit den richtigen Bibliothekskomponenten, sollte es möglich sein , für die Benutzer einen Bereich mit einer minimalen Schnittstelle zu definieren (zB
current
, done
und next
Mitglieder), und haben Iteratortypen automatisch generiert. Eine solche Vorlage für eine Fassadenklasse bleibt als zukünftige Arbeit übrig.
Im Wesentlichen ist dies gleich D-Stil Bereiche (wobei diese Primitive genannt werden empty
, front
und popFront
). Ein begrenzter 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 kann man dies an einen Bereich anpassen, der mit range- verwendet werden kann for
? Hier ist eine Möglichkeit (siehe auch die Reihe von Blog-Posts von @EricNiebler) und die Kommentare von @TC:
#include <iostream>
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()
{
for (auto const& c : RangeAdaptor<PrimitiveStringRange<'!'>>{"Hello World!"})
std::cout << c;
}
Live-Beispiel mit g ++ -std = c ++ 1z ( Assembly mit gcc.godbolt.org)
Schlussfolgerung : Sentinels sind nicht nur ein nützlicher Mechanismus, um Trennzeichen in das Typsystem zu drücken, sie sind allgemein genug, um primitive Bereiche im "D-Stil" (die selbst möglicherweise keine Vorstellung von Iteratoren haben) als Null-Overhead-Abstraktion für das neue C zu unterstützen ++ 1z Bereich für.