C++20: Pythonisch mit der Ranges-Bibliothek
"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:
range
erzeugt eine Liste "containing an arithmetic progression of integers" (Pythons built-in help).filter
wendet ein PrĂ€dikat auf eine Sequenz an und gibt die Elemente zurĂŒck, fĂŒr die das PrĂ€dikat gilt.
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:
- 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.
- 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.
- 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.
range
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:
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
// 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:
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.
Wie geht's weiter?
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
Copyright © 2020 Heise Medien