web-dev-qa-db-fra.com

Pourquoi n'y a-t-il pas de transformation_if dans la bibliothèque standard C++?

Un cas d'utilisation est apparu lorsque vous vouliez faire une copie contitional (1. faisable avec copy_if) mais d'un conteneur de valeurs à un conteneur de pointeurs vers ces valeurs (2. faisable avec transform). 

Avec les outils disponibles, je ne peux pas le faire en moins de deux étapes: 

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

Bien sûr, nous pourrions appeler remove_if sur pv et éliminer le besoin d'un temporaire, mieux encore, il n'est pas difficile de mettre en œuvre (pour les opérations unaires) quelque chose comme ceci: 

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. Existe-t-il une solution de contournement plus élégante avec les outils de bibliothèque standard C++ disponibles?
  2. Existe-t-il une raison pour laquelle transform_if n'existe pas dans la bibliothèque? La combinaison des outils existants constitue-t-elle une solution de contournement suffisante et/ou une performance considérée comme sage?
64
Nikos Athanasiou

La bibliothèque standard privilégie les algorithmes élémentaires.

Les conteneurs et les algorithmes doivent être indépendants les uns des autres si possible.

De même, les algorithmes pouvant être composés d’algorithmes existants ne sont que rarement inclus, en tant que sténographie.

Si vous avez besoin d'une transformation si, vous pouvez l'écrire de manière triviale. Si vous le souhaitez/aujourd’hui /, si vous composez des ready-mades et n’engagez pas de frais généraux, vous pouvez utiliser une bibliothèque de plages comportant plages différées, telles que Boost.Range , par exemple:

v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)

Comme @hvd le fait remarquer dans un commentaire, transform_if aboutit à un type différent (double, dans ce cas). L'ordre de composition est important, et avec Boost Range, vous pouvez également écrire:

 v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)

résultant en une sémantique différente. Cela fait comprendre le point:

cela n'a pas beaucoup de sens d'inclure std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filter etc. etc. dans la bibliothèque standard.

Voir un échantillon Live On Coliru

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

La nouveauté de la notation de boucle réduit à bien des égards le besoin d'algorithmes qui accèdent à tous les éléments de la collection pour lesquels il est maintenant plus simple d'écrire une boucle et de mettre la logique en place.

std::vector< decltype( op( begin(coll) ) > output;
for( auto const& elem : coll )
{
   if( pred( elem ) )
   {
        output.Push_back( op( elem ) );
   }
}

Fournit-il vraiment beaucoup de valeur maintenant pour mettre en place un algorithme? Bien que oui, l'algorithme aurait été utile pour C++ 03 et j'en avais un, mais nous n'en avons pas besoin maintenant, donc aucun avantage réel à l'ajouter.

Notez que, dans la pratique, votre code ne ressemblera pas toujours exactement à cela non plus: vous n’avez pas nécessairement les fonctions "op" et "pred" et vous devrez peut-être créer des lambdas pour les "adapter" aux algorithmes. S'il est agréable de séparer les problèmes si la logique est complexe, s'il ne s'agit que d'extraire un membre du type d'entrée et de vérifier sa valeur ou de l'ajouter à la collection, c'est encore une fois beaucoup plus simple que d'utiliser un algorithme.

De plus, une fois que vous ajoutez une sorte de transformation_if, vous devez décider d'appliquer le prédicat avant ou après la transformation, ou même d'avoir 2 prédicats et de l'appliquer aux deux endroits.

Alors qu'allons-nous faire? Ajouter 3 algorithmes? (Et dans le cas où le compilateur pourrait appliquer le prédicat à l'une des extrémités de la conversion, un utilisateur pourrait facilement choisir le mauvais algorithme par erreur et le code sera toujours compilé mais produira de mauvais résultats).

De même, si les collections sont volumineuses, l'utilisateur souhaite-t-il effectuer une boucle avec des itérateurs ou mapper/réduire? Avec l'introduction de map/réduire, vous obtenez encore plus de complexité dans l'équation.

Essentiellement, la bibliothèque fournit les outils et l'utilisateur est laissé ici pour les utiliser à sa guise, et non l'inverse, comme c'était souvent le cas avec les algorithmes. (Voyez comment l'utilisateur ci-dessus a essayé de tordre les choses en utilisant l'accumulation pour s'adapter à ce qu'il voulait vraiment faire). 

Pour un exemple simple, une carte. Pour chaque élément, je vais afficher la valeur si la clé est paire.

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

Sympa et simple. Envie d’intégrer cela dans un algorithme transform_if?

6
CashCow

La norme est conçue de manière à minimiser les doubles emplois.

Dans ce cas particulier, vous pouvez atteindre les objectifs de l'algorithme de manière plus lisible et plus succincte avec une simple boucle à distance.

// another way

vector<ha*> newVec;
for(auto& item : v) {
    if (item.i < 2) {
        newVec.Push_back(&item);
    }
}

J'ai modifié l'exemple pour qu'il compile, ajouté quelques diagnostics et présenté l'algorithme du PO et le mien côte à côte.

#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;
}
4
Richard Hodges

Désolé de ressusciter cette question après si longtemps. J'ai eu une exigence similaire récemment. Je l'ai résolu en écrivant une version de back_insert_iterator qui prend un coup de pouce :: facultatif:

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

utilisé comme ceci:

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 };
          });
3
Richard Hodges

Après avoir juste retrouvé cette question après un certain temps, et imaginant toute une série d’adaptateurs d’itérateurs génériques potentiellement utiles je me suis rendu compte que la question initiale ne nécessitait RIEN de plus que std::reference_wrapper.

Utilisez-le au lieu d'un pointeur, et vous êtes bon:

Live On Colir

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

Impressions

1 1 
2
sehe

Ceci est juste une réponse à la question 1 "Existe-t-il une solution plus élégante avec les outils de bibliothèque standard C++ disponibles?".

Si vous pouvez utiliser c ++ 17, vous pouvez utiliser std::optional pour une solution plus simple utilisant uniquement la fonctionnalité de bibliothèque standard C++. L'idée est de renvoyer std::nullopt au cas où il n'y aurait pas de mappage:

Voir en direct sur Coliru

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

Notez que je viens d'implémenter l'approche de Rust en C++.

0
Jonny Dee

Vous pouvez utiliser copy_if le long. Pourquoi pas? Définir OutputIt (voir copier ):

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

et réécrivez votre code:

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

Utilisation: (Notez que CONDITION et TRANSFORM ne sont pas des macros, ce sont des espaces réservés pour la condition et la transformation que vous souhaitez appliquer)

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