Возник случай использования, когда нужно сделать условную копию (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
на 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.
- Есть ли более элегантный обходной путь с доступными инструментами стандартной библиотеки C ++?
- Есть ли причина, по которой
transform_if
его нет в библиотеке? Является ли комбинация существующих инструментов достаточным обходным решением и / или считается ли производительность хорошей?
c++
c++-standard-library
stl-algorithm
Никос Афанасиу
источник
источник
transform_if
подразумевает «преобразование только в том случае, если выполняется определенный предикат». Было бы более наглядное название того, что вы хотитеcopy_if_and_transform
!copy_if
также подразумевает «копировать только в том случае, если выполняется определенный предикат». Это одинаково неоднозначно.copy_if
значит, правда ?transform_if
скопировать те элементы, которые он не трансформирует, если трансформация может относиться к другому несовместимому типу? Реализация в вопросе - это именно то, что я ожидал от такой функции.Ответы:
Стандартная библиотека отдает предпочтение элементарным алгоритмам.
Контейнеры и алгоритмы должны быть по возможности независимыми друг от друга.
Точно так же алгоритмы, которые могут быть составлены из существующих алгоритмов, используются в качестве сокращений только изредка.
Если вам требуется преобразование if, вы можете просто написать его. Если вы хотите / сегодня /, составляя готовые модели и не неся накладные расходы, вы можете использовать библиотеку диапазонов с ленивыми диапазонами , например Boost.Range , например:
v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)
Как указывает @hvd в комментарии,
transform_if
double приведет к другому типу (double
в данном случае). Порядок композиции имеет значение, и с помощью Boost Range вы также можете написать:v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)
что приводит к разной семантике. Это подчеркивает суть:
Посмотреть образец 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")); }
источник
boost::phoenix
такие хорошие предикаты без лямбда-выражений? Быстрый поиск в Google не дал ничего подходящего. Благодаря!transform_if
. Это называетсяfilter_map
. Однако я должен признать, что он существует для упрощения кода, но, с другой стороны, можно применить тот же аргумент в случае С ++.Новая нотация цикла 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?
источник
Извините, что воскресил этот вопрос спустя столько времени. Недавно у меня было подобное требование. Я решил это, написав версию back_insert_iterator, которая принимает boost :: 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 }; });
источник
Стандарт разработан таким образом, чтобы минимизировать дублирование.
В этом конкретном случае вы можете достичь целей алгоритма более читаемым и лаконичным способом с помощью простого цикла 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; }
источник
После того, как я снова нашел этот вопрос через некоторое время и разработал целый ряд потенциально полезных универсальных адаптеров итераторов, я понял, что исходный вопрос не требовал НИЧЕГО больше, чем
std::reference_wrapper
.Используйте его вместо указателя, и все будет хорошо:
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 << " "; }
Печать
1 1
источник
Вы можете использовать
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; }
источник
*static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
является одновременно нечитаемым и излишне конкретным. См. Этот дубль С ++ 17 с более общим использованием.std::insert_iterator<>
или,std::ostream_iterator<>
например,), а также позволяет вам предоставить преобразование (например, как лямбда). c ++ 17,for_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; }
Использование: (обратите внимание, что CONDITION и TRANSFORM не являются макросами, они являются заполнителями для любых условий и преобразований, которые вы хотите применить)
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 } );
источник
Это просто ответ на вопрос 1 «Есть ли более элегантный обходной путь с доступными инструментами стандартной библиотеки C ++?».
Если вы можете использовать C ++ 17, вы можете использовать
std::optional
более простое решение, используя только функциональные возможности стандартной библиотеки C ++. Идея состоит в том, чтобы вернутьсяstd::nullopt
в случае отсутствия сопоставления:Смотрите в прямом эфире на 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; }
Обратите внимание, что здесь я только что реализовал подход Rust на C ++.
источник
Вы можете использовать
std::accumulate
which работает с указателем на целевой контейнер: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 << " "; } }
Печать
1 1
источник