zurück zum Artikel

C++20: Pythonisch mit der Ranges-Bibliothek

Rainer Grimm

"Modernes C++"-Blogger Rainer Grimm wagt ein kleines Experiment, und zwar implementiert er beliebte Funktionen in Python mit der Ranges-Bibliothek.

Heute starte ich ein kleines Experiment. Ich will sehr beliebte Funktionen in Python mithilfe der Ranges-Bibliothek implementieren. Ich bin neugierig, wie leicht mir die Umsetzung von Python in C++ fÀllt.

Seit 2004 gebe ich Python-Schulungen. Die Sprache besitzt beeindruckende Funktionen. Daher ist Python fĂŒr mich die Messlatte fĂŒr eine angenehm zu verwendende Programmiersprache. Heute werde ich die Python-Funktionen range und filter implementieren:

C++20: Pythonisch mit der Ranges Bibliothek

Eine Sequenz ist ein Begriff in Python, der fĂŒr etwas steht, ĂŒber das iteriert werden kann. Dies kann eine Liste ([1, 2, 3]), ein Tupel ((1, 2, 3)) oder ein String ("123") sein. Anstelle einer Liste kommt in diesem Artikel ein std::vector in C++ zum Einsatz. Die Funktion filter steht fĂŒr die funktionale Ader in Python.

Bevor ich beginne, möchte ich noch ein paar Bemerkungen loswerden:

  1. Ich verwende in meinen Beispielen Eric Niebers range-v3-Bibliothek, die die Basis fĂŒr die C++20-Ranges-Bibliothek ist. Im vorherigen Artikel "C++20: Die Ranges-Bibliothek [1]" habe ich gezeigt, wie sich das Beispiel in C++20 ĂŒbersetzen lĂ€sst.
  2. Der Python-Code ist oft kĂŒrzer als das C++-Pendant. DafĂŒr gibt es zwei einfache GrĂŒnde: Ich speichere die Listen in Python in keiner Variable ab und gebe sie nicht aus.
  3. Ich möchte keinen Glaubenskrieg zu Programmiersprachen starten. Daher werde ich auf entsprechende Kommentare nicht reagieren.

Jetzt geht es aber los mit der range-Funktion. Sie ist das "Schweizer Taschenmesser" in Python fĂŒr das Erzeugen ganzer Zahlen.

Das folgende Beispiel zeige erst das Python-Beispiel in auskommentierter Form und anschließend das C++ Pendant:

// range.cpp

#include <iostream>
#include <range/v3/all.hpp>
#include <vector>

std::vector<int> range(int begin, int end, int stepsize = 1) {
std::vector<int> result{};
if (begin < end) { // (5)
auto boundary = [end](int i){ return i < end; };
for (int i: ranges::views::iota(begin)
| ranges::views::stride(stepsize)
| ranges::views::take_while(boundary)) {
result.push_back(i);
}
}
else { // (6)
begin++;
end++;
stepsize *= -1;
auto boundary = [begin](int i){ return i < begin; };
for (int i: ranges::views::iota(end)
| ranges::views::take_while(boundary)
| ranges::views::reverse
| ranges::views::stride(stepsize)) {
result.push_back(i);
}
}
return result;
}

int main() {

std::cout << std::endl;

// range(1, 50) // (1)
auto res = range(1, 50);
for (auto i: res) std::cout << i << " ";

std::cout << "\n\n";

// range(1, 50, 5) // (2)
res = range(1, 50, 5);
for (auto i: res) std::cout << i << " ";

std::cout << "\n\n";

// range(50, 10, -1) // (3)
res = range(50, 10, -1);
for (auto i: res) std::cout << i << " ";

std::cout << "\n\n";

// range(50, 10, -5) // (4)
res = range(50, 10, -5);
for (auto i: res) std::cout << i << " ";

std::cout << "\n\n";

}

Die Zeilen (1) bis (4) sollten einfach zu lesen sein, wenn du ihre Ausgabe betrachtest:

C++20: Pythonisch mit der Ranges Bibliothek

Die ersten zwei Argumente fĂŒr den range-Aufruf stehen fĂŒr den Anfang und das Ende der zu erzeugenden Integrale. Dabei ist der Anfang inklusiv und das Ende exklusiv. Die Schrittweite ist das dritte Argument, das per default 1 ist. Wenn der Intervall [begin, end] absteigend ist, sollte die Schrittweite negativ sein. Falls nicht, erhĂ€ltst du eine leere Liste oder einen leeren std::vector<int>.

Ich habe in meinem Beispiel ein wenig geschummelt. Die Funktion ranges::views::stride(n), die nicht Bestandteil von C++20 ist, gibt das n-te Element eines Ranges zurĂŒck. Schreibe mir, wenn du eine elegante Implementierung basierend auf C++20 kennst.

Die if-Bedingung (begin < end) der range-Funktion in Zeile (1) sollte einfach zu verdauen sein. Erzeuge alle Integrale, die mit begin starten (ranges::views::iota(begin)), entnehme jedes n-te Element (ranges::views::stride(stepsize)) und fahre so lange fort, bis die Stopp-Bedingung eintritt (ranges::views::take_while(boundary)). Schiebe die Elemente zum Abschluss auf den std::vector<int>.

Im else-Fall (Zeile 2) musste ich einen Trick anwenden. Der Algorithmus erzeugt die Integrale [end++, begin++], schreitet voran, bis die Stopp-Bedingung erreicht ist, kehrt ihre Reihenfolge um (ranges::views::reverse) und entnimmt jedes n-te Element.

Das Beipiel verwendet die gierige Version von filter und map (dazu mehr im nĂ€chsten Artikel). Mit Python 3 wird filter und map lazy. map und lazy geben in diesem Fall nur Generatoren zurĂŒck. Um das gierige Verhalten von Python 2 zu erzwingen, muss in Python 3 daher ein list-Aufruf um die filter- und map-Aufrufe gesetzt werden.

filter(lambda i: (i % 2) == 1 , range(1, 10))       # Python 2   

list(filter(lambda i: (i % 2) == 1 , range(1, 10))) # Python 3

Beide Aufrufe erzeugen dieselbe Liste: [1, 3, 5, 7, 9].

Nun geht es mit der Funktion filter weiter, da sie einfacher als die Funktion map zu implementieren ist.

// filter.cpp

#include "range.hpp" // (1)

#include <fstream>
#include <iostream>
#include <range/v3/all.hpp>
#include <sstream>
#include <string>
#include <vector>
#include <utility>

template <typename Func, typename Seq> // (2)
auto filter(Func func, Seq seq) {

typedef typename Seq::value_type value_type;

std::vector<value_type> result{};
for (auto i : seq | ranges::views::filter(func)) result.push_back(i);

return result;
}


int main() {

std::cout << std::endl;

// filter(lambda i: (i % 3) == 0 , range(20, 50)) // (3)
auto res = filter([](int i){ return (i % 3) == 0; }, range(20, 50) );
for (auto v: res) std::cout << v << " ";

// (4)
// filter(lambda word: word[0].isupper(), ["Only", "for", "testing", "purpose"])
std::vector<std::string> myStrings{"Only", "for", "testing", "purpose"};
auto res2 = filter([](const std::string& s){ return static_cast<bool>(std::isupper(s[0])); }, myStrings);

std::cout << "\n\n";

for (auto word: res2) std::cout << word << std::endl;

std::cout << std::endl;

// (5)
// len(filter(lambda line: line[0] == "#", open("/etc/services").readlines()))
std::ifstream file("/etc/services", std::ios::in);
std::vector lines;
std::string line;
while(std::getline(file, line)){
lines.push_back(line);
}
std::vector<std::string> commentLines = filter([](const std::string& s){ return s[0] == '#'; }, lines);

std::cout << "Comment lines: " << commentLines.size() << "\n\n";

}

Bevor ich das Programm erlÀutere, möchte ich seine Ausgabe vorstellen:

C++20: Pythonisch mit der Ranges Bibliothek

In diesem Beispiel wird die range-Implementierung nur inkludiert (Zeile 1). Die filter-Funktion in Zeile 2 sollte einfach zu lesen sein. Sie wendet lediglich die aufrufbare Einheit func auf jedes Element der Sequenz an und materialisiert sie in dem std::vector. Die Zeile 3 erzeugt alle Zahlen von 20 bis 50, fĂŒr die (i % 3) == 0 gilt. Nur die Strings, die mit einem Großbuchstaben beginnen, ĂŒberwinden den Filter in Zeile (4). Die Zeile (5) zĂ€hlt, wie viele Zeilen in der Datei "/etc/services" Kommentare sind. Kommentare sind Zeilen, die mit "'#" beginnen.

Wenn du die verschiedenen Arten, Lambdas in Python und C++ zu erzeugen, ignorierst, sind die filter Aufrufe sehr Àhnlich.

map war deutlich schwieriger, als filter in C++ zu implementieren. Einerseits kann map den Typ der Inputsequenz Àndern. Andererseits verursachte meine map-Implemtierung einen GCC-Bugreport. Danach kombiniere ich die Funktionen map und filter in einer Funktion und erhalte ... . Die Details gibt es in meinem nÀchsten Artikel. ( [2])


URL dieses Artikels:
https://www.heise.de/-4671777

Links in diesem Artikel:
[1] https://heise.de/-4661566
[2] mailto:rainer@grimm-jaud.de