Rimuovere elementi da un contenitore o chiedere se un contenitore associativo ha una chiave specifica è troppo complicato. Dovrei dire perché con C++ 20 la storia cambia.
Vorrei iniziare in modo semplice. Vuoi cancellare un elemento da un contenitore.
Il modo di dire cancella-rimuovi
Bene. Rimuovere un elemento da un contenitore è abbastanza semplice. In caso di un std::vecto
r puoi usare la funzione std::remove.
// 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(), [](int ele){ return ele < 0; }); // (1) for (auto ele: myVec) std::cout << ele << " "; std::cout << "\n\n"; }
Il programma removeElemtens.cpp
rimuove tutti gli elementi std::vector
che è minore di zero. Facile, o? Ora, cadi nella trappola che è ben nota a ogni programmatore C++ professionista.
std::remove
o std::remove_if
inline (1) non rimuove nulla. Il std::vector
ha ancora lo stesso numero di argomenti. Entrambi gli algoritmi restituiscono la nuova fine logica del contenitore modificato.
Per modificare un contenitore, devi applicare la nuova estremità logica al contenitore.
// 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"; }
La riga (1) restituisce la nuova fine logica newEnd
del contenitore myVec
. Questa nuova fine logica viene applicata nella riga (2) per rimuovere tutti gli elementi da myVec
a partire da newEnd
. Quando applichi le funzioni remove e cancella in un'espressione come nella riga (3), vedi esattamente perché questo costrutto è chiamato erase-remove-idiom.
Grazie alle nuove funzioni erase
e erase_if
in C++20, cancellare gli elementi dai contenitori è molto più conveniente.
erase
e erase_if
in C++20
Con erase
e erase_if
, puoi operare direttamente sul container. Al contrario, il precedente linguaggio cancella-rimuovi presentato è piuttosto dettagliato (riga 3 in eraseRemoveElements.cpp
):erase
richiede due iteratori che ho fornito dall'algoritmo std::remove_if
.
Vediamo quali sono le nuove funzioni erase
e erase_if
significa in pratica. Il seguente programma cancella elementi per alcuni contenitori.
// 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); }
La riga (1) cancella tutti i caratteri e
dalla stringa data str.
La riga (2) applica l'espressione lambda alla stessa stringa e cancella tutte le lettere maiuscole.
Nel programma rimanente, elementi della sequenza contenitori std::vecto
r (riga 3), std::deque
(riga 4) e std::list
(riga 5) vengono cancellati. Su ogni contenitore, il modello di funzione doAll
(riga 6) si applica. doAll
cancella l'elemento 5 e tutti gli elementi maggiori di 3. Il modello di funzione erase
(riga 7) usa la nuova funzione erase
e il modello di funzione erasePredicate
(riga 8) usa la nuova funzione erase_if
.
Grazie al compilatore Microsoft, ecco l'output del programma.
Le nuove funzioni erase
e erase_if
può essere applicato a tutti i contenitori della Standard Template Library. Questo non vale per la prossima funzione di convenienza contains
.
Verifica dell'esistenza di un elemento in un contenitore associativo
Grazie alle funzioni contains
, puoi facilmente verificare se esiste un elemento in un contenitore associativo.
Stopp, potresti dire, possiamo già farlo con trova o conta.
No, entrambe le funzioni non sono adatte ai principianti e hanno i loro svantaggi.
// 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; }
Le funzioni producono il risultato atteso.
Ecco i problemi con entrambe le chiamate. Il find
call inline (1) è troppo dettagliato. La stessa argomentazione vale per il count
chiamare in linea (2). Il count
la chiamata ha anche un problema di prestazioni. Quando vuoi sapere se un elemento è in un contenitore, dovresti fermarti quando lo hai trovato e non contare fino alla fine. Nel caso concreto myMultiSet.count(2)
restituito 2.
Al contrario, la funzione membro contiene in C++20 è abbastanza comoda da usare.
// 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; }
Non c'è molto da aggiungere a questo esempio. Il modello di funzione containsElement5
restituisce true
se il contenitore associativo contiene la chiave 5. Nel mio esempio ho utilizzato solo i contenitori associativi std::set
, std::unordered_set
, std::map
e std::unordered_set
che non può avere una chiave più di una volta.
Cosa c'è dopo?
Le funzioni di convenienza continuano nel mio prossimo post. Con C++20, puoi calcolare il punto medio di due valori, controlla se un std::string
inizia o finisce con una sottostringa e crea callable con std::bind_front
.