Surgió un caso de uso cuando se deseaba hacer una copia conticional (1. factible con copy_if
) pero desde un contenedor de valores a un contenedor de punteros a esos valores (2. factible con transform
).
Con las herramientas disponibles no puedo hacerlo en menos de dos pasos:
#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;
}
Por supuesto que podríamos llamar remove_if
en pv
y eliminar la necesidad de un temporal, mejor aún, sin embargo, no es difícil de poner en práctica (para las operaciones unarios) algo como esto:
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.
- ¿Existe una solución alternativa más elegante con las herramientas de biblioteca estándar de C ++ disponibles?
- ¿Hay alguna razón por la
transform_if
que no existe en la biblioteca? ¿La combinación de las herramientas existentes es una solución alternativa suficiente y / o se considera que se comporta bien en términos de rendimiento?
c++
c++-standard-library
stl-algorithm
Nikos Athanasiou
fuente
fuente
transform_if
implica "solo transformar si se satisface un determinado predicado". ¡Un nombre más descriptivo para lo que quieres seríacopy_if_and_transform
!copy_if
también implica "solo copiar si se cumple un determinado predicado". Es igualmente ambiguo.copy_if
hace, ¿verdad?transform_if
copiar esos elementos que no transforma, si la transformación puede ser a un tipo incompatible diferente? La implementación en la pregunta es exactamente lo que esperaría que hiciera dicha función.Respuestas:
La biblioteca estándar favorece los algoritmos elementales.
Los contenedores y los algoritmos deben ser independientes entre sí, si es posible.
Del mismo modo, los algoritmos que pueden estar compuestos por algoritmos existentes rara vez se incluyen como forma abreviada.
Si necesita una transformación si, puede escribirla trivialmente. Si lo desea / today /, componiendo ready-mades y no incurrir en gastos generales, puede usar una biblioteca de rango que tenga rangos perezosos , como Boost.Range , por ejemplo:
v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)
Como @hvd señala en un comentario, el
transform_if
resultado doble en un tipo diferente (double
, en este caso). El orden de composición es importante, y con Boost Range también puede escribir:v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)
resultando en una semántica diferente. Esto lleva a casa el punto:
Ver una muestra 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")); }
fuente
boost::phoenix
para hacer predicados tan agradables sin lambdas? Una búsqueda rápida en Google no arrojó nada relevante. ¡Gracias!transform_if
. Se llamafilter_map
. Sin embargo, debo admitir que está ahí para simplificar el código pero, por otro lado, se podría aplicar el mismo argumento en el caso de C ++.La nueva notación de bucle for reduce de muchas maneras la necesidad de algoritmos que accedan a cada elemento de la colección donde ahora es más limpio escribir un bucle y poner la lógica en su lugar.
std::vector< decltype( op( begin(coll) ) > output; for( auto const& elem : coll ) { if( pred( elem ) ) { output.push_back( op( elem ) ); } }
¿Realmente proporciona mucho valor ahora poner un algoritmo? Si bien sí, el algoritmo habría sido útil para C ++ 03 y, de hecho, tenía uno para él, no necesitamos uno ahora, por lo que no hay una ventaja real en agregarlo.
Tenga en cuenta que, en el uso práctico, su código tampoco siempre se verá exactamente así: no necesariamente tiene funciones "op" y "pred" y es posible que deba crear lambdas para que "encajen" en los algoritmos. Si bien es bueno separar las preocupaciones si la lógica es compleja, si solo se trata de extraer un miembro del tipo de entrada y verificar su valor o agregarlo a la colección, es mucho más simple una vez más que usar un algoritmo.
Además, una vez que agregas algún tipo de transform_if, debes decidir si aplicar el predicado antes o después de la transformación, o incluso tener 2 predicados y aplicarlo en ambos lugares.
¿Entonces, que vamos a hacer? ¿Agregar 3 algoritmos? (Y en el caso de que el compilador pudiera aplicar el predicado en cualquier extremo de la conversión, un usuario podría elegir fácilmente el algoritmo equivocado por error y el código aún se compila pero produce resultados incorrectos).
Además, si las colecciones son grandes, ¿el usuario desea hacer un bucle con iteradores o mapear / reducir? Con la introducción de map / reduce, obtienes aún más complejidades en la ecuación.
Esencialmente, la biblioteca proporciona las herramientas, y el usuario se queda aquí para usarlas para que se ajusten a lo que quiere hacer, no al revés, como suele ocurrir con los algoritmos. (Vea cómo el usuario anterior intentó cambiar las cosas usando acumular para que se ajustaran a lo que realmente quería hacer).
Para un ejemplo simple, un mapa. Para cada elemento, generaré el valor si la clave es par.
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; }
Agradable y sencillo. ¿Te apetece incluir eso en un algoritmo transform_if?
fuente
Lamento resucitar esta pregunta después de tanto tiempo. Recientemente tuve un requisito similar. Lo resolví escribiendo una versión de back_insert_iterator que toma un impulso :: opcional:
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); }
usado así:
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 }; });
fuente
El estándar está diseñado de tal manera que se minimizan las duplicaciones.
En este caso particular, puede lograr los objetivos del algoritmo de una manera más legible y concisa con un simple ciclo de rango para.
// another way vector<ha*> newVec; for(auto& item : v) { if (item.i < 2) { newVec.push_back(&item); } }
Modifiqué el ejemplo para que se compile, agregué algunos diagnósticos y presenté tanto el algoritmo del OP como el mío uno al lado del otro.
#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; }
fuente
Después de encontrar esta pregunta nuevamente después de un tiempo, e idear una gran cantidad de adaptadores de iteradores genéricos potencialmente útiles, me di cuenta de que la pregunta original no requería NADA más que
std::reference_wrapper
.Úselo en lugar de un puntero, y estará bien:
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 << " "; }
Huellas dactilares
1 1
fuente
Puede usarlo
copy_if
. Por qué no? DefinirOutputIt
(ver copia ):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; } };
y reescribe tu código:
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; }
fuente
*static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
es ilegible e innecesariamente concreto. Vea esta toma de c ++ 17 con usos más genéricos.std::insert_iterator<>
o,std::ostream_iterator<>
por ejemplo) y también le permite proporcionar una transformación (por ejemplo, como una lambda). c ++ 17, comienza a parecer útil / Igual en 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; }
Uso: (Tenga en cuenta que CONDITION y TRANSFORM no son macros, son marcadores de posición para cualquier condición y transformación que desee aplicar)
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 } );
fuente
Esta es solo una respuesta a la pregunta 1 "¿Existe una solución alternativa más elegante con las herramientas de biblioteca estándar de C ++ disponibles?".
Si puede usar c ++ 17, puede usarlo
std::optional
para una solución más simple usando solo la funcionalidad de biblioteca estándar de C ++. La idea es volverstd::nullopt
en caso de que no haya mapeo:Ver en vivo en 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; }
Tenga en cuenta que acabo de implementar el enfoque de Rust en C ++ aquí.
fuente
Puede usar
std::accumulate
que opera en un puntero al contenedor de destino:Live On Coliru
#include <numeric> #include <iostream> #include <vector> struct ha { int i; }; // filter and transform is here std::vector<int> * fx(std::vector<int> *a, struct ha const & v) { if (v.i < 2) { a->push_back(v.i); } return a; } int main() { std::vector<ha> v { {1}, {7}, {1}, }; std::vector<int> ph; // target vector std::accumulate(v.begin(), v.end(), &ph, fx); for (int el : ph) { std::cout << el << " "; } }
Huellas dactilares
1 1
fuente