¿Por qué no hay transform_if en la biblioteca estándar de C ++?


82

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_ifen pvy 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.
  1. ¿Existe una solución alternativa más elegante con las herramientas de biblioteca estándar de C ++ disponibles?
  2. ¿Hay alguna razón por la transform_ifque 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?

(OMI) El nombre transform_ifimplica "solo transformar si se satisface un determinado predicado". ¡Un nombre más descriptivo para lo que quieres sería copy_if_and_transform!
Oliver Charlesworth

@OliCharlesworth, en realidad copy_iftambién implica "solo copiar si se cumple un determinado predicado". Es igualmente ambiguo.
Shahbaz

@Shahbaz: Pero eso es lo que copy_ifhace, ¿verdad?
Oliver Charlesworth

2
¡No me sorprendería que las disputas sobre el nombre de tal cosa fueran la razón real para no implementarlo!
Nikos Athanasiou

6
Tal vez me falte algo en estos comentarios, pero ¿cómo podría transform_ifcopiar 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:


32

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_ifresultado 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:

que tiene muy poco sentido de incluir std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filteretc., etc., en la biblioteca estándar .

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

26
Bueno, el problema es que los algoritmos estándar no se pueden componer fácilmente, porque no son perezosos.
Jan Hudec

1
@JanHudec De hecho. (¿Lo siento por eso? :)). Es por eso que usa una biblioteca (al igual que usa AMP / TBB para simultaneidad o Extensiones reactivas en C #). Mucha gente está trabajando en una propuesta de rango + implementación para su inclusión en el estándar.
sehe

2
@sehe +1 Muy impresionante, ¡he aprendido algo nuevo hoy! ¿Sería tan amable de decirnos a quienes no están familiarizados con Boost.Range y Phoenix dónde podemos encontrar la documentación / ejemplos que explican cómo usar boost::phoenixpara hacer predicados tan agradables sin lambdas? Una búsqueda rápida en Google no arrojó nada relevante. ¡Gracias!
Ali

1
No estoy de acuerdo con respecto a la parte "tiene muy poco sentido incluir std :: filter_and_transform". Otros lenguajes de programación también proporcionan esta combinación en su "biblioteca estándar". Tiene mucho sentido iterar sobre una lista de elementos una vez, transformándolos sobre la marcha, mientras se omiten los que no se pueden transformar. Otros enfoques requieren más de una pasada. Sí, puede usar BOOST, pero la pregunta en realidad era "¿Por qué no hay transform_if en la biblioteca estándar de C ++?". Y en mi humilde opinión, tiene razón al cuestionar esto. Debería haber una función de este tipo en la biblioteca estándar.
Jonny Dee

1
@sehe Con respecto a "todos usan abstracciones componibles": eso no es cierto. Rust, por ejemplo, tiene exactamente tal transform_if. Se llama filter_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 ++.
Jonny Dee

6

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?


3
Si cree que mi código anterior tiene más espacio para errores que un transform_if con 2 lambdas, uno para el predicado y otro para la transformación, entonces explíquelo. Ensamblador, C y C ++ son lenguajes diferentes y tienen lugares diferentes. El único lugar en el que el algoritmo puede tener una ventaja sobre un bucle es la capacidad de "mapear / reducir" para que se ejecute simultáneamente en grandes colecciones. Sin embargo, de esta manera, el usuario puede controlar si realizar un bucle en secuencia o reducir el mapa.
CashCow

3
En un enfoque funcional adecuado, las funciones de predicado y mutador son bloques bien definidos que hacen que el constructo esté estructurado correctamente. El cuerpo del bucle puede tener elementos arbitrarios, y cada bucle que ve debe analizarse cuidadosamente para comprender su comportamiento.
Bartek Banachewicz

2
Deje el enfoque funcional adecuado para los lenguajes funcionales adecuados. Esto es C ++.
CashCow

3
"¿Te apetece incluir eso en un algoritmo transform_if?" Ese es un "algoritmo transform_if", excepto que tiene todo codificado.
R. Martinho Fernandes

2
Realiza el equivalente a transform_if. Solo que se supone que los algoritmos simplifican su código o lo mejoran de alguna manera, no lo hacen más complicado.
CashCow

5

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

1
No medido: hasta que los usuarios se quejan de que su experiencia depende de la CPU (es decir, nunca), me preocupa más la corrección que los nanosegundos. Sin embargo, no veo que sea pobre. Los opcionales son muy baratos ya que no hay asignación de memoria y solo se llama al constructor Ts si el opcional está realmente poblado. Esperaría que el optimizador elimine casi todo el código muerto, ya que todas las rutas de código son visibles en el momento de la compilación.
Richard Hodges

Si. Estoy de acuerdo si no se tratara exactamente de un algoritmo de propósito general (en realidad, un bloque de construcción genérico dentro de esos). Este es el lugar donde normalmente no estoy entusiasmado a menos que algo sea tan simple como parece. Además, me encantaría que el manejo opcional sea un decorador en cualquier iterador de salida (por lo que al menos obtenemos componibilidad de los iteradores de salida, mientras intentamos tapar la falta de componibilidad de los algoritmos).
Sehe

Lógicamente, no hay diferencia si maneja la inserción opcional a través de un decorador en el iterador o en la función de transformación. En última instancia, es solo una prueba de una bandera. Creo que encontrará que el código optimizado sería el mismo de cualquier manera. Lo único que se interpone en el camino de la optimización completa sería el manejo de excepciones. Marcar T como sin constructores excepto los curaría esto.
Richard Hodges

¿Qué forma le gustaría que tomara la llamada a transform ()? Estoy seguro de que podríamos construir una suite de iteradores componibles.
Richard Hodges

Yo también :) Estaba comentando tu sugerencia. No estaba proponiendo otra cosa (lo hice hace mucho tiempo. En su lugar, tengamos rangos y algoritmos componibles :))
vea el

3

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

3

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 

1

Puede usarlo copy_if. Por qué no? Definir OutputIt(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;
}

3
"¿Por qué no?" - Porque el código es para humanos. Para mí, la fricción es peor que volver a escribir objetos de función en lugar de lambdas. *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.
sehe

Aquí hay una versión que no codifica el iterador base (por lo que puede usarlo con 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 ++ 11
vea el

Tenga en cuenta que, en este punto, hay pocas razones para mantener los iteradores base, y puede simplemente: usar cualquier función , notando que Boost contiene una mejor implementación: boost :: function_output_iterator . Ahora todo lo que queda es reinventar for_each_if:)
vea el

En realidad, releyendo la pregunta original, agreguemos una voz de razón , usando solo la biblioteca estándar de c ++ 11.
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;
}

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

¿Calificaría esta producción de implementación lista? ¿Funcionaría bien con elementos no copiables? ¿O mover iteradores?
sehe

0

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::optionalpara una solución más simple usando solo la funcionalidad de biblioteca estándar de C ++. La idea es volver std::nullopten 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í.


0

Puede usar std::accumulateque 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 
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.