Warum gibt es in der C ++ - Standardbibliothek kein transform_if?


82

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_ifauf pvund 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.
  1. Gibt es eine elegantere Problemumgehung mit den verfügbaren C ++ - Standardbibliothekswerkzeugen?
  2. Gibt es einen Grund, warum transform_ifes in der Bibliothek keine gibt? Ist die Kombination der vorhandenen Tools eine ausreichende Problemumgehung und / oder wird sie als leistungsmäßig angesehen?

(IMO) Der Name transform_ifimpliziert "nur transformieren, wenn ein bestimmtes Prädikat erfüllt ist". Ein aussagekräftigerer Name für das, was Sie wollen, wäre copy_if_and_transform!
Oliver Charlesworth

@OliCharlesworth copy_ifimpliziert eigentlich auch "nur kopieren, wenn ein bestimmtes Prädikat erfüllt ist". Es ist ebenso mehrdeutig.
Shahbaz

@ Shahbaz: Aber genau das copy_ifmacht es, richtig?
Oliver Charlesworth

2
Ich wäre nicht überrascht, wenn Streitigkeiten über den Namen einer solchen Sache der eigentliche Grund wären, sie nicht umzusetzen !!
Nikos Athanasiou

6
Vielleicht fehlt mir etwas in diesen Kommentaren, aber wie könnte man transform_ifmö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:


32

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_ifdouble zu einem anderen Typ ( doublein 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:

es macht wenig Sinn zu schließen std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filteretc. etc. in die Standardbibliothek .

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"));
}

26
Das Problem ist, dass die Standardalgorithmen nicht einfach zusammengesetzt werden können, weil sie nicht faul sind.
Jan Hudec

1
@ JanHudec In der Tat. (Das tut mir leid? :)). Aus diesem Grund verwenden Sie eine Bibliothek (ähnlich wie Sie AMP / TBB für Parallelität oder Reactive Extensions in C # verwenden). Viele Leute arbeiten an einem Range Proposition + Implementierung für die Aufnahme in den Standard.
sehe

2
@sehe +1 Sehr beeindruckend, ich habe heute etwas Neues gelernt! Würden Sie uns so freundlich sagen, wer nicht mit Boost.Range und Phoenix vertraut ist, wo wir die Dokumentation / Beispiele finden, die erklären, wie boost::phoenixman so schöne Prädikate ohne Lambdas macht? Eine schnelle Google-Suche ergab nichts Relevantes. Vielen Dank!
Ali

1
Ich bin nicht einverstanden mit dem Teil "Es macht sehr wenig Sinn, std :: filter_and_transform einzuschließen". Andere Programmiersprachen bieten diese Kombination ebenfalls in ihrer "Standardbibliothek" an. Es ist absolut sinnvoll, eine Liste von Elementen einmal zu durchlaufen, sie im laufenden Betrieb zu transformieren und diejenigen zu überspringen, die nicht transformiert werden können. Andere Ansätze erfordern mehr als einen Durchgang. Ja, Sie können BOOST verwenden, aber die Frage war tatsächlich "Warum gibt es kein transform_if in der C ++ - Standardbibliothek?". Und meiner Meinung nach hat er Recht, dies in Frage zu stellen. Es sollte eine solche Funktion in der Standardbibliothek geben.
Jonny Dee

1
@sehe In Bezug auf "sie alle verwenden zusammensetzbare Abstraktionen": das ist nicht wahr. Rust zum Beispiel hat genau so eine transform_if. Es heißt filter_map. Ich muss jedoch zugeben, dass es zur Vereinfachung des Codes dient, aber andererseits könnte man im C ++ - Fall dasselbe Argument anwenden.
Jonny Dee

6

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?


3
Wenn Sie der Meinung sind, dass mein Code oben mehr Platz für Fehler bietet als ein transform_if mit 2 Lambdas, eines für das Prädikat und eines für die Transformation, dann erklären Sie es bitte. Assembly, C und C ++ sind verschiedene Sprachen und haben unterschiedliche Orte. Der einzige Ort, an dem der Algorithmus gegenüber einer Schleife im Vorteil sein kann, ist die Fähigkeit, "abzubilden / zu reduzieren", sodass er gleichzeitig über große Sammlungen ausgeführt wird. Auf diese Weise kann der Benutzer jedoch steuern, ob eine Schleife nacheinander oder eine Kartenreduzierung durchgeführt werden soll.
CashCow

3
In einem geeigneten funktionalen Ansatz sind Funktionen für Prädikat und Mutator gut definierte Blöcke, die das Konstrukt richtig strukturieren. Denn der Schleifenkörper kann beliebige Dinge enthalten, und jede Schleife, die Sie sehen, muss sorgfältig analysiert werden, um ihr Verhalten zu verstehen.
Bartek Banachewicz

2
Lassen Sie den richtigen funktionalen Ansatz für die richtigen funktionalen Sprachen. Das ist C ++.
CashCow

3
"Möchten Sie das in einen transform_if-Algorithmus integrieren?" Das ist ein "transform_if-Algorithmus", außer dass alles fest codiert ist.
R. Martinho Fernandes

2
Es führt das Äquivalent eines transform_if aus. Nur dass Algorithmen Ihren Code vereinfachen oder auf irgendeine Weise verbessern sollen, nicht komplizierter machen.
CashCow

5

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 };
          });

1
Nicht gemessen - bis Benutzer sich beschweren, dass ihre Erfahrung CPU-gebunden ist (dh niemals), geht es mir mehr um Korrektheit als um Nanosekunden. Ich kann jedoch nicht sehen, dass es arm ist. Optionals sind sehr billig, da es keine Speicherzuordnung gibt und der Ts-Konstruktor nur aufgerufen wird, wenn das Optional tatsächlich ausgefüllt ist. Ich würde erwarten, dass der Optimierer fast den gesamten toten Code eliminiert, da alle Codepfade zur Kompilierungszeit sichtbar sind.
Richard Hodges

Ja. Ich würde zustimmen, wenn es nicht genau um einen Allzweckalgorithmus geht (eigentlich ein generischer Baustein in diesen). Dies ist der Ort, an dem ich normalerweise nicht begeistert bin, es sei denn, etwas ist so einfach wie es nur geht. Außerdem würde ich es begrüßen, wenn die optionale Behandlung ein Dekorator für jeden Ausgabe-Iterator wäre (so erhalten wir zumindest die Kompositionsfähigkeit von Ausgabe-Iteratoren, während wir versuchen, die mangelnde Kompositionsfähigkeit von Algorithmen zu beseitigen).
sehe

Es gibt logischerweise keinen Unterschied, ob Sie die optionale Einfügung über einen Dekorator auf der Iteration oder in der Transformationsfunktion behandeln. Es ist letztendlich nur ein Test einer Flagge. Ich denke, Sie werden feststellen, dass der optimierte Code in beiden Fällen der gleiche ist. Das einzige, was einer vollständigen Optimierung im Wege steht, ist die Ausnahmebehandlung. Das Markieren von T als mit Ausnahme von Konstruktoren würde dies heilen.
Richard Hodges

Welche Form soll der Aufruf von transform () annehmen? Ich bin sicher, wir könnten eine zusammensetzbare Iteratorsuite erstellen.
Richard Hodges

Ich auch :) Ich habe Ihren Vorschlag kommentiert. Ich habe nichts anderes vorgeschlagen (das hatte ich schon vor langer Zeit. Lassen Sie uns stattdessen Bereiche und zusammensetzbare Algorithmen haben :))
sehe

3

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;
}

3

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 

1

Sie können mit verwenden copy_if. Warum nicht? Definieren OutputIt(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;
}

3
"Warum nicht?" - Weil Code für Menschen ist. Für mich ist die Reibung tatsächlich schlimmer als das Zurückschreiben von Funktionsobjekten anstelle von Lambdas. *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.
sehe

In dieser Version wird der Basis-Iterator nicht fest codiert (Sie können ihn also mit std::insert_iterator<>oder std::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 ++ 11
sehe

Beachten Sie, dass es zu diesem Zeitpunkt kaum einen Grund gibt, die Basisiteratoren beizubehalten, und Sie können einfach: eine beliebige Funktion verwenden und feststellen, dass Boost eine bessere Implementierung enthält: boost :: function_output_iterator . Jetzt müssen for_each_if
wir

Wenn wir die ursprüngliche Frage noch einmal lesen, fügen wir eine Stimme der Vernunft hinzu - nur mit der C ++ 11-Standardbibliothek.
sehe

0
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
    }
);

Würden Sie diese Implementierungsproduktion als fertig bewerten? Würde es gut mit nicht kopierbaren Elementen funktionieren? Oder Bewegungsiteratoren?
sehe

0

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::optionaleine einfachere Lösung verwenden, indem Sie nur die C ++ - Standardbibliotheksfunktionalität verwenden. Die Idee ist, zurückzukehren, std::nulloptfalls 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.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.