¿Por qué no hay transformación si en la biblioteca estándar de C++?


Surgió un caso de uso cuando se quería hacer una copia conticional (1. factible con copy_if) pero de 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, podríamos llamar a remove_if en pv y eliminar la necesidad de un temporal, mejor aún, no es difícil implementar (para operaciones unarias) 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. ¿Hay una solución más elegante con las herramientas de biblioteca estándar de C++ disponibles ?
  2. ¿Hay alguna razón por la que transform_if no existe en la biblioteca? ¿La combinación de las herramientas existentes es una solución suficiente y / o se considera que el rendimiento se comporta bien ?
Author: einpoklum, 2014-05-10

8 answers

La biblioteca estándar favorece los algoritmos elementales.

Los contenedores y algoritmos deben ser independientes entre sí si es posible.

Del mismo modo, los algoritmos que pueden estar compuestos de algoritmos existentes solo se incluyen raramente, como abreviatura.

Si necesita una transformación if, puede escribirla trivialmente. Si lo desea / today/, componiendo de ready-mades y no incurrir en sobrecarga, puede usar una biblioteca de rangos que tenga rangos perezosos, como Boost.Rango, por ejemplo:

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

Como @hvd señala en un comentario, transform_if doble resultado en un tipo diferente (double, en este caso). El orden de la composición importa, y con Boost Range también puedes escribir:

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

Resultando en diferentes semánticas. Esto nos lleva al punto:

tiene muy poco sentido incluir std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filter etc. sucesivamente. en la biblioteca estándar.

Ver una muestra Vivir En 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"));
}
 28
Author: sehe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-09 12:03:48

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 para poner en 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, así que no hay una ventaja real en agregarlo.

Tenga en cuenta que en el uso práctico su código no siempre se verá exactamente así tampoco: no necesariamente tiene funciones" op "y" pred "y puede que tenga que crear lambdas para que" encajen " en algoritmos. Si bien es bueno separar las preocupaciones si la lógica es compleja, si es solo una cuestión de extraer un miembro del tipo de entrada y verificar su valor o agregarlo a la colección, es mucho más simple que usar un algoritmo.

Además, una vez que esté agregando algún tipo de transform_if, debe decidir si aplicar el predicado antes o después de la transformación, o incluso tener 2 predicados y aplicarlo en ambos lugares.

Entonces, ¿qué vamos a hacer? Agregar 3 algoritmos? (Y en el caso de que el compilador pudiera aplicar el predicado en cualquiera de los extremos de la conversión, un usuario podría fácilmente elegir el algoritmo equivocado por error y el código aún compilar pero producir resultados incorrectos).

Además, si las colecciones son grandes, ¿el usuario desea realizar un bucle con iteradores o mapear/reducir? Con la introducción de map/reduce obtenga aún más complejidades en la ecuación.

Esencialmente, la biblioteca proporciona las herramientas, y el usuario se deja aquí para usarlas para adaptarse a lo que quiere hacer, no al revés, como solía ser el caso con los algoritmos. (Ver cómo el usuario de arriba trató de torcer las cosas utilizando acumular para adaptarse a lo que realmente quería hacer).

Para un ejemplo simple, un mapa. Para cada elemento voy a 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 simple. Fancy ajuste que en un transform_if algoritmo?

 5
Author: CashCow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-09 10:57:05

El estándar está diseñado de tal manera que se minimice la duplicación.

En este caso particular, puede lograr los objetivos del algoritmo de una manera más legible y sucinta con un bucle range-for simple.

// another way

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

He modificado el ejemplo para que compile, agregue algunos diagnósticos y presente tanto el algoritmo de OP como el mío lado a lado.

#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
Author: Richard Hodges,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-10 10:58:52

Lamento resucitar esta pregunta después de tanto tiempo. Tuve un requisito similar recientemente. 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 };
          });
 3
Author: Richard Hodges,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-07 11:43:07

Después de encontrar esta pregunta de nuevo después de algún tiempo, y idear toda una serie de adaptadores iteradores genéricos potencialmente útiles Me di cuenta de que la pregunta original no requería nada más que std::reference_wrapper.

Úsalo en lugar de un puntero, y estarás bien:

Vivir En 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 << " ";
}

Imprime

1 1 
 1
Author: sehe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-09 11:02:59
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 la CONDICIÓN y la TRANSFORMACIÓN 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
    }
);
 0
Author: user5406764,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-21 14:33:49

Puede usar copy_if junto. ¿Por qué no? Define OutputIt (ver copiar):

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;
}
 0
Author: dyomas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-12-08 16:24:57

Esto es solo una respuesta a la pregunta 1 "¿Hay una solución más elegante con las herramientas de biblioteca estándar de C++ disponibles ?".

Si puede usar c++17, entonces puede usar std::optional para una solución más simple utilizando solo la funcionalidad de la biblioteca estándar de C++. La idea es devolver std::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í.

 0
Author: Jonny Dee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-28 18:30:09