Ein Anwendungsfall trat auf, wenn eine bedingte Kopie (1. machbar mit copy_if
), aber von einem Wertecontainer zu einem Container mit Zeigern auf diese Werte (2. machbar mit transform
) erstellt werden soll.
Mit den verfügbaren Tools kann ich es nicht in weniger als zwei Schritten tun :
#include <vector>
#include <algorithm>
using namespace std;
struct ha {
int i;
explicit ha(int a) : i(a) {}
};
int main()
{
vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
// GOAL : make a vector of pointers to elements with i < 2
vector<ha*> ph; // target vector
vector<ha*> pv; // temporary vector
// 1.
transform(v.begin(), v.end(), back_inserter(pv),
[](ha &arg) { return &arg; });
// 2.
copy_if(pv.begin(), pv.end(), back_inserter(ph),
[](ha *parg) { return parg->i < 2; }); // 2.
return 0;
}
Ofcourse wir nennen könnte remove_if
auf pv
und eliminieren die Notwendigkeit für eine temporäre, besser jedoch noch, dann ist es nicht schwer zu implementieren (für einstellige Operationen) so etwas wie folgt aus :
template <
class InputIterator, class OutputIterator,
class UnaryOperator, class Pred
>
OutputIterator transform_if(InputIterator first1, InputIterator last1,
OutputIterator result, UnaryOperator op, Pred pred)
{
while (first1 != last1)
{
if (pred(*first1)) {
*result = op(*first1);
++result;
}
++first1;
}
return result;
}
// example call
transform_if(v.begin(), v.end(), back_inserter(ph),
[](ha &arg) { return &arg; }, // 1.
[](ha &arg) { return arg.i < 2; });// 2.
- Gibt es eine elegantere Problemumgehung mit den verfügbaren C ++ - Standardbibliothekswerkzeugen?
- Gibt es einen Grund, warum
transform_if
es in der Bibliothek keine gibt? Ist die Kombination der vorhandenen Tools eine ausreichende Problemumgehung und / oder wird sie als leistungsmäßig angesehen?
c++
c++-standard-library
stl-algorithm
Nikos Athanasiou
quelle
quelle
transform_if
impliziert "nur transformieren, wenn ein bestimmtes Prädikat erfüllt ist". Ein aussagekräftigerer Name für das, was Sie wollen, wärecopy_if_and_transform
!copy_if
impliziert eigentlich auch "nur kopieren, wenn ein bestimmtes Prädikat erfüllt ist". Es ist ebenso mehrdeutig.copy_if
macht es, richtig?transform_if
möglicherweise die Elemente kopieren, die nicht transformiert werden, wenn die Transformation in einen anderen inkompatiblen Typ erfolgen kann? Die Implementierung in der Frage ist genau das, was ich von einer solchen Funktion erwarten würde.Antworten:
Die Standardbibliothek bevorzugt elementare Algorithmen.
Container und Algorithmen sollten nach Möglichkeit unabhängig voneinander sein.
Ebenso werden Algorithmen, die aus vorhandenen Algorithmen zusammengesetzt werden können, nur selten als Kurzform aufgenommen.
Wenn Sie eine Transformation benötigen, können Sie sie trivial schreiben. Wenn Sie möchten, dass es / heute / aus fertigen Elementen besteht und kein Overhead entsteht, können Sie eine Bereichsbibliothek verwenden, die über verzögerte Bereiche verfügt , z. B. Boost.Range , z.
v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)
Wie @hvd in einem Kommentar hervorhebt, führt
transform_if
double zu einem anderen Typ (double
in diesem Fall). Die Reihenfolge der Komposition ist wichtig, und mit Boost Range können Sie auch schreiben:v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)
was zu unterschiedlicher Semantik führt. Dies bringt den Punkt nach Hause:
Sehen Sie sich ein Beispiel Live On Coliru an
#include <boost/range/algorithm.hpp> #include <boost/range/adaptors.hpp> using namespace boost::adaptors; // only for succinct predicates without lambdas #include <boost/phoenix.hpp> using namespace boost::phoenix::arg_names; // for demo #include <iostream> int main() { std::vector<int> const v { 1,2,3,4,5 }; boost::copy( v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0), std::ostream_iterator<double>(std::cout, "\n")); }
quelle
boost::phoenix
man so schöne Prädikate ohne Lambdas macht? Eine schnelle Google-Suche ergab nichts Relevantes. Vielen Dank!transform_if
. Es heißtfilter_map
. Ich muss jedoch zugeben, dass es zur Vereinfachung des Codes dient, aber andererseits könnte man im C ++ - Fall dasselbe Argument anwenden.Die neue for-Schleifen-Notation reduziert in vielerlei Hinsicht den Bedarf an Algorithmen, die auf jedes Element der Sammlung zugreifen, wo es jetzt sauberer ist, einfach eine Schleife zu schreiben und die Logik zu installieren.
std::vector< decltype( op( begin(coll) ) > output; for( auto const& elem : coll ) { if( pred( elem ) ) { output.push_back( op( elem ) ); } }
Bietet es jetzt wirklich viel Wert, einen Algorithmus einzufügen? Ja, der Algorithmus wäre zwar für C ++ 03 nützlich gewesen, und ich hatte tatsächlich einen dafür, aber wir brauchen jetzt keinen, also keinen wirklichen Vorteil beim Hinzufügen.
Beachten Sie, dass Ihr Code im praktischen Gebrauch auch nicht immer genau so aussieht: Sie haben nicht unbedingt die Funktionen "op" und "pred" und müssen möglicherweise Lambdas erstellen, damit sie in Algorithmen "passen". Es ist zwar schön, Bedenken auszuräumen, wenn die Logik komplex ist, aber wenn es nur darum geht, ein Element aus dem Eingabetyp zu extrahieren und seinen Wert zu überprüfen oder es der Sammlung hinzuzufügen, ist es wieder viel einfacher als die Verwendung eines Algorithmus.
Wenn Sie eine Art transform_if hinzufügen, müssen Sie außerdem entscheiden, ob Sie das Prädikat vor oder nach der Transformation anwenden möchten oder ob Sie sogar zwei Prädikate haben und es an beiden Stellen anwenden möchten.
So, was werden wir machen? 3 Algorithmen hinzufügen? (Und für den Fall, dass der Compiler das Prädikat an beiden Enden der Konvertierung anwenden könnte, könnte ein Benutzer leicht versehentlich den falschen Algorithmus auswählen und der Code wird immer noch kompiliert, führt jedoch zu falschen Ergebnissen.)
Wenn die Sammlungen groß sind, möchte der Benutzer dann eine Schleife mit Iteratoren erstellen oder zuordnen / reduzieren? Mit der Einführung von Map / Reduce erhalten Sie noch mehr Komplexität in der Gleichung.
Im Wesentlichen stellt die Bibliothek die Tools zur Verfügung, und der Benutzer kann sie hier verwenden, um sie an das anzupassen, was er tun möchte, und nicht umgekehrt, wie dies bei Algorithmen häufig der Fall war. (Sehen Sie, wie der Benutzer oben versucht hat, die Dinge mithilfe von Akkumulieren zu verdrehen, um sie an das anzupassen, was er wirklich tun wollte.)
Für ein einfaches Beispiel eine Karte. Für jedes Element gebe ich den Wert aus, wenn der Schlüssel gerade ist.
std::vector< std::string > valuesOfEvenKeys ( std::map< int, std::string > const& keyValues ) { std::vector< std::string > res; for( auto const& elem: keyValues ) { if( elem.first % 2 == 0 ) { res.push_back( elem.second ); } } return res; }
Schön und einfach. Möchten Sie das in einen transform_if-Algorithmus integrieren?
quelle
Tut mir leid, diese Frage nach so langer Zeit wieder zu beleben. Ich hatte kürzlich eine ähnliche Anforderung. Ich habe es gelöst, indem ich eine Version von back_insert_iterator geschrieben habe, die einen Schub braucht :: optional:
template<class Container> struct optional_back_insert_iterator : public std::iterator< std::output_iterator_tag, void, void, void, void > { explicit optional_back_insert_iterator( Container& c ) : container(std::addressof(c)) {} using value_type = typename Container::value_type; optional_back_insert_iterator<Container>& operator=( const boost::optional<value_type> opt ) { if (opt) { container->push_back(std::move(opt.value())); } return *this; } optional_back_insert_iterator<Container>& operator*() { return *this; } optional_back_insert_iterator<Container>& operator++() { return *this; } optional_back_insert_iterator<Container>& operator++(int) { return *this; } protected: Container* container; }; template<class Container> optional_back_insert_iterator<Container> optional_back_inserter(Container& container) { return optional_back_insert_iterator<Container>(container); }
so verwendet:
transform(begin(s), end(s), optional_back_inserter(d), [](const auto& s) -> boost::optional<size_t> { if (s.length() > 1) return { s.length() * 2 }; else return { boost::none }; });
quelle
Der Standard ist so konzipiert, dass Doppelarbeit minimiert wird.
In diesem speziellen Fall können Sie die Ziele des Algorithmus mit einer einfachen Range-for-Schleife lesbarer und prägnanter erreichen.
// another way vector<ha*> newVec; for(auto& item : v) { if (item.i < 2) { newVec.push_back(&item); } }
Ich habe das Beispiel so geändert, dass es kompiliert, einige Diagnosen hinzugefügt und sowohl den OP-Algorithmus als auch meinen nebeneinander dargestellt.
#include <vector> #include <algorithm> #include <iostream> #include <iterator> using namespace std; struct ha { explicit ha(int a) : i(a) {} int i; // added this to solve compile error }; // added diagnostic helpers ostream& operator<<(ostream& os, const ha& t) { os << "{ " << t.i << " }"; return os; } ostream& operator<<(ostream& os, const ha* t) { os << "&" << *t; return os; } int main() { vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector // GOAL : make a vector of pointers to elements with i < 2 vector<ha*> ph; // target vector vector<ha*> pv; // temporary vector // 1. transform(v.begin(), v.end(), back_inserter(pv), [](ha &arg) { return &arg; }); // 2. copy_if(pv.begin(), pv.end(), back_inserter(ph), [](ha *parg) { return parg->i < 2; }); // 2. // output diagnostics copy(begin(v), end(v), ostream_iterator<ha>(cout)); cout << endl; copy(begin(ph), end(ph), ostream_iterator<ha*>(cout)); cout << endl; // another way vector<ha*> newVec; for(auto& item : v) { if (item.i < 2) { newVec.push_back(&item); } } // diagnostics copy(begin(newVec), end(newVec), ostream_iterator<ha*>(cout)); cout << endl; return 0; }
quelle
Nachdem ich diese Frage nach einiger Zeit wiedergefunden und eine ganze Reihe potenziell nützlicher generischer Iteratoradapter entwickelt hatte, wurde mir klar, dass die ursprüngliche Frage NICHTS mehr als erforderte
std::reference_wrapper
.Verwenden Sie es anstelle eines Zeigers, und Sie sind gut:
Live On Coliru
#include <algorithm> #include <functional> // std::reference_wrapper #include <iostream> #include <vector> struct ha { int i; }; int main() { std::vector<ha> v { {1}, {7}, {1}, }; std::vector<std::reference_wrapper<ha const> > ph; // target vector copy_if(v.begin(), v.end(), back_inserter(ph), [](const ha &parg) { return parg.i < 2; }); for (ha const& el : ph) std::cout << el.i << " "; }
Druckt
1 1
quelle
Sie können mit verwenden
copy_if
. Warum nicht? DefinierenOutputIt
(siehe Kopie ):struct my_inserter: back_insert_iterator<vector<ha *>> { my_inserter(vector<ha *> &dst) : back_insert_iterator<vector<ha *>>(back_inserter<vector<ha *>>(dst)) { } my_inserter &operator *() { return *this; } my_inserter &operator =(ha &arg) { *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg; return *this; } };
und schreiben Sie Ihren Code neu:
int main() { vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector // GOAL : make a vector of pointers to elements with i < 2 vector<ha*> ph; // target vector my_inserter yes(ph); copy_if(v.begin(), v.end(), yes, [](const ha &parg) { return parg.i < 2; }); return 0; }
quelle
*static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
ist sowohl unlesbar als auch unnötig konkret. Sehen Sie sich diese C ++ 17- Version mit allgemeineren Verwendungen an.std::insert_iterator<>
oderstd::ostream_iterator<>
z. B. verwenden) und Sie können auch eine Transformation angeben (z. B. als Lambda). c ++ 17, beginnt nützlich auszusehen / Gleiches in c ++ 11for_each_if
template <class InputIt, class OutputIt, class BinaryOp> OutputIt transform_if(InputIt it, InputIt end, OutputIt oit, BinaryOp op) { for(; it != end; ++it, (void) ++oit) op(oit, *it); return oit; }
Verwendung: (Beachten Sie, dass CONDITION und TRANSFORM keine Makros sind, sondern Platzhalter für die Bedingungen und Transformationen, die Sie anwenden möchten.)
std::vector a{1, 2, 3, 4}; std::vector b; return transform_if(a.begin(), a.end(), b.begin(), [](auto oit, auto item) // Note the use of 'auto' to make life easier { if(CONDITION(item)) // Here's the 'if' part *oit++ = TRANSFORM(item); // Here's the 'transform' part } );
quelle
Dies ist nur eine Antwort auf Frage 1 "Gibt es eine elegantere Problemumgehung mit den verfügbaren C ++ - Standardbibliothekswerkzeugen?".
Wenn Sie c ++ 17 verwenden können, können Sie
std::optional
eine einfachere Lösung verwenden, indem Sie nur die C ++ - Standardbibliotheksfunktionalität verwenden. Die Idee ist, zurückzukehren,std::nullopt
falls es keine Zuordnung gibt:Live auf Coliru sehen
#include <iostream> #include <optional> #include <vector> template < class InputIterator, class OutputIterator, class UnaryOperator > OutputIterator filter_transform(InputIterator first1, InputIterator last1, OutputIterator result, UnaryOperator op) { while (first1 != last1) { if (auto mapped = op(*first1)) { *result = std::move(mapped.value()); ++result; } ++first1; } return result; } struct ha { int i; explicit ha(int a) : i(a) {} }; int main() { std::vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector // GOAL : make a vector of pointers to elements with i < 2 std::vector<ha*> ph; // target vector filter_transform(v.begin(), v.end(), back_inserter(ph), [](ha &arg) { return arg.i < 2 ? std::make_optional(&arg) : std::nullopt; }); for (auto p : ph) std::cout << p->i << std::endl; return 0; }
Beachten Sie, dass ich hier gerade Rusts Ansatz in C ++ implementiert habe.
quelle