Neue praktische Funktionen für Container in C++20

Modernes C++ Rainer Grimm  –  5 Kommentare

Elemente aus einem Container zu entfernen oder einen assoziativen Container zu fragen, ob er ein Element besitzt, war bislang zu kompliziert. Mit C++20 ändert sich das.

Mein Artikel beginnt mit einer einfachen Aufgabe. Ich möchte Elemente eines Containers entfernen.

Das erase-remove-Idiom

Das Entfernen der Elemente eines Containers ist eine einfache Aufgabe. Im Fall eines std::vector bietet es sich an, die Funktion std::remove einzusetzen:

// removeElements.cpp

#include <algorithm>
#include <iostream>
#include <vector>

int main() {

std::cout << std::endl;

std::vector myVec{-2, 3, -5, 10, 3, 0, -5 };

for (auto ele: myVec) std::cout << ele << " ";
std::cout << "\n\n";

std::remove_if(myVec.begin(), myVec.end(), // (1)
[](int ele){ return ele < 0; });
for (auto ele: myVec) std::cout << ele << " ";

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

}

Das Programm removeElements.cpp entfernt alle Elemente des std::vector, die kleiner als null sind. Einfach, oder? Nein, du bist in die Falle getappt, die wohl schon jeder professionelle C++-Entwickler kennengelernt hat.

std::remove und std::remove_if in Zeile (1) entfernen nichts aus dem Vektor. std::vector besitzt immer noch dieselbe Anzahl an Elementen. Beide Algorithmen geben das neue logische Ende des veränderten Containers zurück. Um einen Container zu verändern, musst du das neue logische Ende auf den Container anwenden:

// eraseRemoveElements.cpp

#include <algorithm>
#include <iostream>
#include <vector>

int main() {

std::cout << std::endl;

std::vector myVec{-2, 3, -5, 10, 3, 0, -5 };

for (auto ele: myVec) std::cout << ele << " ";
std::cout << "\n\n";

auto newEnd = std::remove_if(myVec.begin(), myVec.end(), // (1)
[](int ele){ return ele < 0; });
myVec.erase(newEnd, myVec.end()); // (2)
// myVec.erase(std::remove_if(myVec.begin(), myVec.end(), // (3)
[](int ele){ return ele < 0; }), myVec.end());
for (auto ele: myVec) std::cout << ele << " ";

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

}

Zeile (1) gibt das neue logische Ende newEnd des Containers myVec zurück. Das neue logische Ende wird daraufhin in Zeile (2) angewendet, um alle Elemente von myVec zu löschen, die bei newEnd beginnen. Wenn nun die Funktionen remove und erase wie in Zeile (3) angewendet werden, wird klar, warum dieses Konstrukt erase-remove-Idiom genannt wird.

Dank der neuen Funktionen erase und erase_if wird das Entfernen von Elementes eines Containers deutlich einfacher.

erase und erase_if in C++20

Mit erase und erase_if können die Elemente des Containers direkt gelöscht werden. Im Gegensatz dazu ist das vorgestellte erase-remove-Idiom (Zeile 3 in eraseRemovElements.cpp) sehr umständlich: erase benötigt zwei Iteratoren, die durch std::remove_if erzeugt werden.

Lass mich zeigen, wie sich die zwei neuen Funktionen erase und erase_if in der Anwendung schlagen. Das folgende Programm entfernt Elemente von verschiedenen Containern:

// eraseCpp20.cpp

#include <iostream>
#include <numeric>
#include <deque>
#include <list>
#include <string>
#include <vector>

template <typename Cont> // (7)
void eraseVal(Cont& cont, int val) {
std::erase(cont, val);
}

template <typename Cont, typename Pred> // (8)
void erasePredicate(Cont& cont, Pred pred) {
std::erase_if(cont, pred);
}

template <typename Cont>
void printContainer(Cont& cont) {
for (auto c: cont) std::cout << c << " ";
std::cout << std::endl;
}

template <typename Cont> // (6)
void doAll(Cont& cont) {
printContainer(cont);
eraseVal(cont, 5);
printContainer(cont);
erasePredicate(cont, [](auto i) { return i >= 3; } );
printContainer(cont);
}

int main() {

std::cout << std::endl;

std::string str{"A Sentence with an E."};
std::cout << "str: " << str << std::endl;
std::erase(str, 'e'); // (1)
std::cout << "str: " << str << std::endl;
std::erase_if( str, [](char c){ return std::isupper(c); }); // (2)
std::cout << "str: " << str << std::endl;

std::cout << "\nstd::vector " << std::endl;
std::vector vec{1, 2, 3, 4, 5, 6, 7, 8, 9}; // (3)
doAll(vec);

std::cout << "\nstd::deque " << std::endl;
std::deque deq{1, 2, 3, 4, 5, 6, 7, 8, 9}; // (4)
doAll(deq);

std::cout << "\nstd::list" << std::endl;
std::list lst{1, 2, 3, 4, 5, 6, 7, 8, 9}; // (5)
doAll(lst);

}

Die Zeile (1) entfernt alle Buchstaben e des Strings str. Zeile (2) wendet den Lambda-Ausdruck auf denselben String an und entfernt alle Großbuchstaben.

Im verbleibenden Programm werden Elemente der Sequenz-Container std::vector (Zeile 3), std::deque (Zeile 4) und std::list (Zeile 5) entfernt. Auf jeden Container wird das Funktions-Template doAll (line 6) angewandt. doAll entfernt das Element 5 und anschließend alle Elemente größer als 3. Das Funktions-Template erase setzt die neue Funktion erase (Zeile 7) ein, und das Funktions-Template erasePredicate (Zeile 8) greift auf die neue Funktion erase_if zurück.

Dank des Microsoft Compiler kann ich die Ausgabe des Programms präsentieren:

Die vorgestellten neuen Funktionen erase und erase_if lassen sich auf alle Container der Standard Template Library anwenden. Das gilt nicht für die nächste praktische Funktion contains.

Prüfen, ob ein assoziativer Container ein Element enthält

Dank der Funktion contains kannst du einfach prüfen, ob ein assoziativer Container ein bestimmtes Element enthält. Ich höre schon die ersten Einsprüche, denn dies ist doch bereits mit den Funktionen find oder count möglich. Nein. Beide Funktionen sind nicht benutzerfreundlich und besitzen weitere Nachteile:

// checkExistens.cpp

#include <set>
#include <iostream>

int main() {

std::cout << std::endl;

std::set mySet{3, 2, 1};
if (mySet.find(2) != mySet.end()) { // (1)
std::cout << "2 inside" << std::endl;
}

std::multiset myMultiSet{3, 2, 1, 2};
if (myMultiSet.count(2)) { // (2)
std::cout << "2 inside" << std::endl;
}

std::cout << std::endl;

}

Die Funktionen liefern das erwartete Ergebnis:

Das sind die Probleme der beiden Funktionen. Der find-Aufruf in Zeile (1) ist zu wortreich. Derselbe Punkt gilt natürlich auch für den count-Aufruf in Zeile (2). Der count-Aufruf besitzt dazu noch ein Performanzproblem. Wenn du wissen willst, ob ein Container ein Element enthält, solltest du deine Suche dann beenden, wenn du das Element gefunden hast. count zählt aber tapfer bis zum Ende des Containers weiter. In der konkreten Anwendung gibt der Aufruf myMultSet.count(2) die 2 zurück.

Im Gegensatz dazu ist die contains-Methode sehr angenehm zu verwenden:

// containsElement.cpp

#include <iostream>
#include <set>
#include <map>
#include <unordered_set>
#include <unordered_map>

template <typename AssozCont>
bool containsElement5(const AssozCont& assozCont) { // (1)
return assozCont.contains(5);
}

int main() {

std::cout << std::boolalpha;

std::cout << std::endl;

std::set<int> mySet{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "containsElement5(mySet): " << containsElement5(mySet);

std::cout << std::endl;

std::unordered_set<int> myUnordSet{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::cout << "containsElement5(myUnordSet): " << containsElement5(myUnordSet);

std::cout << std::endl;

std::map<int, std::string> myMap{ {1, "red"}, {2, "blue"}, {3, "green"} };
std::cout << "containsElement5(myMap): " << containsElement5(myMap);

std::cout << std::endl;

std::unordered_map<int, std::string> myUnordMap{ {1, "red"}, {2, "blue"}, {3, "green"} };
std::cout << "containsElement5(myUnordMap): " << containsElement5(myUnordMap);

std::cout << std::endl;

}

Das Beispiel benötigt keine wortreiche Erklärung. Das Funktions-Template containsElement5 gibt true zurück, wenn der assoziative Container den Schlüssel 5 besitzt. In meinem Beispiel habe ich nur die assoziativen Container std::set, std::unordered_set, std::map und std::unordered_set verwendet, die einen Schlüssel nicht mehrmals enthalten können.

Wie geht's weiter?

Mein nächster Artikel dreht sich weiter um die praktischen Funktionen. In C++20 lässt sich der Mittelpunkt zweier Werte berechnen, prüfen, ob ein String mit einem Teilstring beginnt oder endet, und ein Funktionsobjekt mit der Funktion std::bind_front erzeugen.