Moderne Art, STL-Container zu filtern?

97

Als ich nach Jahren von C # zu C ++ zurückkehrte, fragte ich mich, wie die moderne Methode zum Filtern eines Arrays aussehen würde: Wie können wir etwas Ähnliches wie diese Linq-Abfrage erreichen:

var filteredElements = elements.Where(elm => elm.filterProperty == true);

Um einen Vektor von Elementen zu filtern ( stringsum dieser Frage willen)?

Ich hoffe aufrichtig, dass die alten STL-Algorithmen (oder sogar Erweiterungen wie boost::filter_iterator), bei denen explizite Methoden definiert werden müssen, inzwischen ersetzt werden.

EIN FERNSEHER
quelle
Ruft dies alle Elemente ab, die auf filterPropertyeingestellt sind true?
Joseph Mansfield
Entschuldigung, ja. Einige generische Filterkriterien ..
ATV
3
Es gibt auch einige Bibliotheken, die versuchen, die LINQ-Methoden von .NET zu emulieren: Linq ++ und cpplinq . Ich habe nicht mit ihnen gearbeitet, aber ich vermute, dass sie STL-Container unterstützen.
Dirk
1
Sie sollten klarer sein, was Sie wollen, da die Anzahl der Personen, die sowohl in C ++ als auch in C # kompetent sind, gering ist. Beschreiben Sie, was Sie möchten.
Yakk - Adam Nevraumont

Antworten:

114

Siehe das Beispiel von cplusplus.com für std::copy_if:

std::vector<int> foo = {25,15,5,-5,-15};
std::vector<int> bar;

// copy only positive numbers:
std::copy_if (foo.begin(), foo.end(), std::back_inserter(bar), [](int i){return i>=0;} );

std::copy_ifwertet den Lambda-Ausdruck für jedes Element foohier aus und truekopiert den Wert nach, wenn er zurückgegeben wird bar.

Dies std::back_inserterermöglicht es uns, am Ende von bar(using push_back()) mit einem Iterator tatsächlich neue Elemente einzufügen, ohne die Größe zuerst auf die erforderliche Größe ändern zu müssen.

Sebastian Hoffmann
quelle
30
Ist dies wirklich der LINQ am nächsten, den C ++ zu bieten hat? Dies ist eifrig (IOW nicht faul) und sehr ausführlich.
usr
1
@usr Sein syntaktischer IMO-Zucker, eine einfache for-Schleife, erledigt ebenfalls die Aufgabe (und ermöglicht es oft, das Kopieren zu vermeiden).
Sebastian Hoffmann
1
Das OPs-Beispiel verwendet keinen syntaktischen LINQ-Zucker. Die Vorteile sind eine verzögerte Bewertung und Zusammensetzbarkeit.
usr
1
@usr Was immer noch leicht mit einer einfachen for-Schleife erreicht werden kann, std::copy_ifist nicht mehr als eine for-Schleife
Sebastian Hoffmann
13
@Paranaix Alles kann gesagt werden, nur syntaktischer Zucker über Montage zu sein. Der Punkt ist, nicht für Schleifen zu schreiben, wenn ein Algorithmus unter Verwendung primitiver Operationen (wie Filter) klar lesbar zusammengesetzt werden kann. Viele Sprachen bieten solche Funktionen an - in C ++ ist es leider immer noch klobig.
BartoszKP
47

Ein effizienterer Ansatz ist, wenn Sie keine neue Kopie der Liste benötigen remove_if, die Elemente tatsächlich aus dem ursprünglichen Container zu entfernen.

djhaskin987
quelle
7
@ATV Ich mag es remove_ifbesonders, weil es die Möglichkeit ist, Filter bei Vorhandensein von Mutationen zu verwenden, was schneller ist als das Kopieren einer ganz neuen Liste. Wenn ich Filter in C ++ machen würde, würde ich dies über verwenden copy_if, also denke ich, dass es hinzufügt.
Djhaskin987
16
Zumindest für Vektor remove_ifändert sich das nicht size(). Sie werden es mit der Kette müssen erasedafür .
Rampion
5
@rampion Yeah .. löschen / entfernen. Eine andere Schönheit, die mir häufig das Gefühl gibt, dass ich heutzutage Löcher in ein Band stanze, wenn ich in C ++ arbeite (im Gegensatz zu modernen Sprachen) ;-)
ATV
1
Das explizite Löschen ist eine Funktion. Sie müssen nicht in allen Fällen löschen. Manchmal reichen die Iteratoren aus, um fortzufahren. In solchen Fällen würde ein implizites Löschen unnötigen Overhead verursachen. Darüber hinaus kann nicht jeder Container in der Größe geändert werden. std :: array hat zum Beispiel überhaupt keine Löschmethode.
Martin Fehrs
31

Verwenden Sie in C ++ 20 die Filteransicht aus der Bereichsbibliothek: (erforderlich #include <ranges>)

// namespace views = std::ranges::views;
vec | views::filter([](int a){ return a % 2 == 0; })

gibt die geraden Elemente träge in zurück vec.

(Siehe [range.adaptor.object] / 4 und [range.filter] )


Dies wird bereits von GCC 10 ( Live-Demo ) unterstützt. Für Clang und ältere Versionen von GCC kann auch die ursprüngliche Range-v3-Bibliothek mit #include <range/v3/view/filter.hpp>(oder #include <range/v3/all.hpp>) und dem ranges::viewsNamespace anstelle von std::ranges::views( Live-Demo ) verwendet werden.

LF
quelle
Sie sollten das #include angeben und den Namespace verwenden, der für die Kompilierung Ihrer Antwort erforderlich ist. Welcher Compiler unterstützt dies ab heute?
Gsimard
2
@gsimard Besser jetzt?
LF
1
Wenn jemand versucht, dies unter macOS zu tun: Ab Mai 2020 unterstützt libc ++ dies nicht.
dax
25

Ich denke, Boost.Range verdient auch eine Erwähnung. Der resultierende Code kommt dem Original ziemlich nahe:

#include <boost/range/adaptors.hpp>

// ...

using boost::adaptors::filtered;
auto filteredElements = elements | filtered([](decltype(elements)::value_type const& elm)
    { return elm.filterProperty == true; });

Der einzige Nachteil ist, dass der Parametertyp des Lambda explizit deklariert werden muss. Ich habe decltype (elements) :: value_type verwendet, weil es nicht erforderlich ist, den genauen Typ zu buchstabieren, und außerdem ein Körnchen der Generizität hinzufügt. Alternativ könnte der Typ mit den polymorphen Lambdas von C ++ 14 einfach als auto angegeben werden:

auto filteredElements = elements | filtered([](auto const& elm)
    { return elm.filterProperty == true; });

gefilterte Elemente wären ein Bereich, der zum Durchqueren geeignet ist, aber im Grunde ist es eine Ansicht des ursprünglichen Containers. Wenn Sie einen anderen Container benötigen, der mit Kopien der Elemente gefüllt ist, die die Kriterien erfüllen (so dass er unabhängig von der Lebensdauer des Originalcontainers ist), könnte dies folgendermaßen aussehen:

using std::back_inserter; using boost::copy; using boost::adaptors::filtered;
decltype(elements) filteredElements;
copy(elements | filtered([](decltype(elements)::value_type const& elm)
    { return elm.filterProperty == true; }), back_inserter(filteredElements));
user2478832
quelle
12

Mein Vorschlag für ein C ++ - Äquivalent von C #

var filteredElements = elements.Where(elm => elm.filterProperty == true);

Definieren Sie eine Vorlagenfunktion, an die Sie ein Lambda-Prädikat übergeben, um die Filterung durchzuführen. Die Vorlagenfunktion gibt das gefilterte Ergebnis zurück. z.B:

template<typename T>
vector<T> select_T(const vector<T>& inVec, function<bool(const T&)> predicate)
{
  vector<T> result;
  copy_if(inVec.begin(), inVec.end(), back_inserter(result), predicate);
  return result;
}

zu verwenden - ein triviales Beispiel geben:

std::vector<int> mVec = {1,4,7,8,9,0};

// filter out values > 5
auto gtFive = select_T<int>(mVec, [](auto a) {return (a > 5); });

// or > target
int target = 5;
auto gt = select_T<int>(mVec, [target](auto a) {return (a > target); });
pjm
quelle
11

Verbesserter pjm- Code gemäß Unterstrich-d- Vorschlägen:

template <typename Cont, typename Pred>
Cont filter(const Cont &container, Pred predicate) {
    Cont result;
    std::copy_if(container.begin(), container.end(), std::back_inserter(result), predicate);
    return result;
}

Verwendung:

std::vector<int> myVec = {1,4,7,8,9,0};

auto filteredVec = filter(myVec, [](int a) { return a > 5; });
Alex P.
quelle