Più funzioni utili per i container con C++20

Più funzioni utili per i container con C++20

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 .