Forum: PC Hard- und Software C++ std::ranges custom view


von Vincent H. (vinci)


Lesenswert?

Grüß euch

Ich bin gerade dabei meine ersten Zeilen "Ranges" Code zu schreiben. 
Konkret interessiert mich wie man eigene Views erzeugt und diese mit 
anderen Algorithmen kombinierbar macht. Als Beispiel hab ich mir dafür 
einen "toupper_view" überlegt, der eine Range von Buchstaben nimmt und 
diese als Großbuchstaben wieder ausspuckt.

Soweit sogut. Die erste Version die ich geschrieben hab funktioniert 
auch wunderbar. Es gibt einen
- toupper_view

sowie einen nested
- toupper_view::iterator

Um möglichst wenig Code zu duplizieren hab ich in der ersten Fassung vom 
Iterator Typen der übergebenen Range abgeleitet. Das ist im Regelfall 
der __normal_iterator aus der Standardbibliothek.

Hier der Vollständigkeit halber mal der Iterator Code:
1
template <typename Rng>
2
struct toupper_view<Rng>::iterator : ranges::iterator_t<Rng> {
3
  using base = ranges::iterator_t<Rng>;
4
  using value_type = typename ranges::range_value_t<Rng>;
5
6
  iterator() = default;
7
8
  iterator(base const& b) : base{b} {}
9
10
  iterator operator++(int) { return static_cast<base&>(*this)++; }
11
12
  iterator& operator++() {
13
    ++static_cast<base&>(*this);
14
    return (*this);
15
  }
16
17
  value_type operator*() const {
18
    return std::toupper(*static_cast<base>(*this));
19
  }
20
};



Nun wollte ich zum Üben die Basisklasse aus der Standardbibliothek 
loswerden und hab einen zweiten Iterator ohne Abhängigkeiten 
geschrieben. Jener Iterator arbeitet jedoch nicht mit Pointern sondern 
nimmt die gesamte Range sowie eine Größe als Eingangsparameter.
1
template <typename Rng>
2
struct toupper_view<Rng>::iterator {
3
  using value_type = typename ranges::range_value_t<Rng>;
4
5
  iterator() = default;
6
7
  iterator(Rng rng, size_t count) : rng_{rng}, count_{count} {}
8
9
  iterator operator++(int) {
10
    iterator const retval{*this};
11
    ++count_;
12
    return retval;
13
  }
14
15
  iterator& operator++() {
16
    ++count_;
17
    return *this;
18
  }
19
20
  value_type operator*() const { return std::toupper(rng_[count_]); }
21
22
  bool operator==(iterator const& rhs) const { return count_ == rhs.count_; }
23
24
  bool operator!=(iterator const& rhs) const { return count_ != rhs.count_; }
25
26
 private:
27
  Rng rng_{};
28
  size_t count_{};
29
};


Leider funktioniert der zweite Iterator nur teilweise. Er tut zwar 
prinzipiell wie gewollt, lässt sich jedoch nicht mit anderen 
Range-Algorithmen kombinieren. Er besteht auch weder das Range-, noch 
das View-Concept (siehe https://en.cppreference.com/w/cpp/ranges)...

Konkret dürfte es ihn hierbei aufstellen:
1
template< class T >
2
concept __RangeImpl = requires(T&& t) {
3
  ranges::begin(std::forward<T>(t)); // equality-preserving for forward iterators
4
  ranges::end  (std::forward<T>(t));
5
};
6
7
// ->
8
// begin(static_cast<T&&>(t))' would be ill-formed
9
// end(static_cast<T&&>(t))' would be ill-formed

Ich versteh nur leider überhaupt nicht wieso?


Hier ist der gesamte Code auf godbolt zu finden:
https://godbolt.org/z/BZe6W9

Mit dem Macro "USE_ITERATOR" lässt sich zwischen den beiden Iteratoren 
umschalten.

von M.K. B. (mkbit)


Lesenswert?

Ich habe selbst noch nie aktive mit Ranges gearbeitet, aber ein sehr 
gutes Verständnis von C++17.
Wenn etwas unverständlich ist, dann bitte gerne nachfragen.

In C++ wurden die Funktionen std::begin und std::end eingeführt, um 
einen allgemeinen Weg zu haben sich einen Iterator zu besorgen. Damit 
war es möglich z.B. std::vector<char> und char[] beim abholen des 
Iterators gleich zu behandeln. Mit auto ergibt sich damit der Typ aus 
dem Rückgabewert von begin/end. Ich meine, dass auch range based for 
darauf aufbaut.

Das concept beschwert sich in deinem Fall, dass dein Iterator ohne 
Basisklasse nicht bei begin/end als Parameter übergeben werden kann. 
Vermutlich bekommst du dies durch die Ableitung von 
ranges::iterator_t<Rng>. Mir ist dabei aber unklar, ob der Aufruf von 
operator* dann ranges::iterator_t::operator* oder 
toupper_view::operator* aufruft.
Ich habe aber gerade leider keinen Compiler zur Hand, wo ich das testen 
könnte.

Falls es dir darum geht, die Iteratoren für ranges besser zu verstehen 
kann ich dir leider nicht weiterhelfen.

Für deinen konkreten Fall würde ich aber ein transform für die range 
nehmen, weil du ja die Daten transformieren möchtest. std::toupper 
konnte ich aber nicht direkt aufrufen, sondern musste es in ein lambda 
verpacken, aber ich weiß nicht warum.

Dieses Beispiel kompiliert im Compiler Explorer, aber ich weiß nicht, ob 
es auch das tut, was ich erwarte.
1
#include <stl2/view/transform.hpp>
2
3
auto rng = str | ranges::views::transform([](char c){return std::toupper(c);});

von Vincent H. (vinci)


Lesenswert?

Mein View besitzt sowohl begin als auch end. Der Iterator selbst 
natürlich nicht, wozu auch?

Und ja, natürlich könnte man transform nutzen, aber das ist nicht Sinn 
der Übung.


Übrigens bestzt Compiler Explorer mittlerweile auch einen Output. Somit 
kann man den auch direkt zum Testen nutzen. ;)

von Vincent H. (vinci)


Lesenswert?

Iterator traits haben gefehlt (difference_type, value_type, pointer, 
reference, iterator_category).

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.