Почему в стандартной библиотеке C++ нет преобразования if?


в случае использования возникла, когда желая сделать contitional копия (1. выполнимо с copy_if), но из контейнера значений в контейнер указателей на эти значения (2. выполнимо с transform).

С помощью доступных инструментов я не могу сделать менее чем за два шага :

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

конечно, мы могли бы назвать remove_if on pv и устранить необходимость во временном, еще лучше, хотя, это не трудно реализовать (для унарных операции) что-то вроде этого :

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. есть ли более элегантный обходной путь с доступными средствами стандартной библиотеки C++?
  2. почему transform_if не существует в библиотеке? Является ли комбинация существующих инструментов достаточным обходным путем и / или считается, что производительность хорошо себя ведет ?
8 58

8 ответов:

стандартная библиотека предпочитает элементарные алгоритмы.

контейнеры и алгоритмы должны быть независимы друг от друга, если это возможно.

кроме того, алгоритмы, которые могут быть составлены из существующих алгоритмов, редко включаются в качестве стенографии.

Если вам требуется преобразить, если можно просто написать его. Если вы хотите это /сегодня/, составляя готовые файлы и не нести накладные расходы, вы можете использовать библиотеку диапазона, которая имеет ленивый диапазоны, например как импульс.Диапазон, например:

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

как указывает @hvd в комментарии,transform_if двойной результат в разного типа (double в данном случае). Порядок композиции имеет значение, и с диапазоном усиления вы также можете написать:

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

что приводит к различной семантике. Это приводит домой точку:

это имеет очень мало смысла включить std::filter_and_transform,std::transform_and_filter,std::filter_transform_and_filter etc. так далее. в стандарт библиотека.

посмотреть образец Жить На 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"));
}

новая нотация цикла for во многом уменьшает потребность в алгоритмах, которые обращаются к каждому элементу коллекции, где теперь чище просто написать цикл и поместить логику на место.

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

действительно ли это дает много значения теперь, чтобы положить в алгоритм? Хотя Да, алгоритм был бы полезен для C++03, и действительно, у меня был один для него, теперь он нам не нужен, поэтому нет реального преимущества в его добавлении.

обратите внимание, что в практическом использовании вашего кода не всегда выглядят точно так же: у вас не обязательно есть функции "op" и "pred" и, возможно, придется создавать лямбды, чтобы они "вписывались" в алгоритмы. Хотя приятно отделить проблемы, если логика сложна, если это просто вопрос извлечения элемента из типа ввода и проверки его значения или добавления его в коллекцию, это намного проще, чем использовать алгоритм.

кроме того, как только вы добавляете какой-то transform_if, вы должны решить следует ли применять предикат до или после преобразования, или даже 2 предикаты и применять его в обоих местах.

Так что же мы будем делать? Добавить 3 алгоритмы? (И в случае, если компилятор может применить предикат на любом конце преобразования, пользователь может легко выбрать неправильный алгоритм по ошибке, и код все еще компилируется, но дает неправильные результаты).

кроме того, если коллекции большие, пользователь хочет выполнить цикл с итераторами или сопоставить/уменьшить? С при введении map / reduce вы получаете еще больше сложностей в уравнении.

по сути, библиотека предоставляет инструменты, и пользователь остается здесь, чтобы использовать их, чтобы соответствовать тому, что они хотят сделать, а не наоборот, как это часто бывает с алгоритмами. (Смотрите, как пользователь выше пытался крутить вещи, используя накопление, чтобы соответствовать тому, что они действительно хотели сделать).

для простого примера, карта. Для каждого элемента я выведу значение, если ключ даже.

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

красиво и просто. Необычно вписать это в алгоритм transform_if?

стандарт разработан таким образом, чтобы свести к минимуму дублирование.

в этом конкретном случае вы можете достичь целей алгоритма более читаемым и лаконичным способом с помощью простого цикла range-for.

// another way

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

Я изменил пример, чтобы он компилировался, добавил некоторые диагностики и представил алгоритм OP и мой бок о бок.

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

Извините, что воскрешаю этот вопрос после столь долгого времени. Недавно у меня было аналогичное требование. Я решил это, написав версию back_insert_iterator, которая принимает импульс:: 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);
}

использовать так:

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

после того, как просто найти этот вопрос снова через некоторое время, и разработка целого ряда потенциально полезных универсальных адаптеров итераторов я понял, что первоначальный вопрос не требовал ничего больше, чем std::reference_wrapper.

используйте его вместо указателя, и вы хорошо:

Жить На 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 << " ";
}

печать

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

использование: (обратите внимание, что условие и преобразование не являются макросами, они являются заполнителями для любого условия и преобразования, которые вы хотите применить)

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

вы можете использовать copy_if вместе. почему бы и нет? определение OutputIt (см. скопировать):

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

и переписать свой код:

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

Это просто ответ на вопрос 1 "Есть ли более элегантный обходной путь с помощью доступных инструментов стандартной библиотеки C++?".

Если вы можете использовать c++17, то вы можете использовать std::optional для более простого решения, используя только функциональность стандартной библиотеки C++. Идея состоит в том, чтобы вернуться std::nullopt в случае отсутствия сопоставления:

смотрите в прямом эфире на Колиру

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

обратите внимание, что я только что реализовал Раста!--11--> в C++ здесь.