Чому в стандартній бібліотеці C ++ немає transform_if?


84

Випадок використання виник, коли хотіли зробити умовну копію (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.
  1. Чи є більш елегантне рішення із наявними стандартними інструментами бібліотеки C ++?
  2. Чи є причина, чому transform_ifне існує в бібліотеці? Чи є поєднання існуючих інструментів достатнім обхідним шляхом та / або вважається ефективністю розумного поведінки?

(IMO) Назва transform_ifпередбачає "перетворювати лише в тому випадку, якщо певний присудок задоволений". Більш описова назва того, що ви хочете, буде copy_if_and_transform!
Олівер Чарлсворт

@OliCharlesworth, насправді copy_ifтакож передбачає "копіювати лише в тому випадку, якщо певний присудок задоволений". Це однаково неоднозначно.
Шахбаз

@Shahbaz: Але ось що copy_if, правда?
Олівер Чарлсворт

2
Я б не здивувався, якби суперечки щодо назви такої речі були основною причиною нереалізації цього !!
Нікос Афанасіу

6
Можливо, я щось пропускаю в цих коментарях, але як можна transform_ifскопіювати ті елементи, які він не трансформує, якщо перетворення може бути до іншого несумісного типу? Реалізація у питанні - це саме те, що я очікував би робити така функція.

Відповіді:


33

Стандартна бібліотека надає перевагу елементарним алгоритмам.

Контейнери та алгоритми повинні бути незалежними один від одного, якщо це можливо.

Аналогічно, алгоритми, які можуть складатися з існуючих алгоритмів, рідко включаються як скорочення.

Якщо вам потрібне перетворення if, ви можете просто написати його. Якщо ви хочете це / сьогодні /, складаючи готові файли і не вимагаючи накладних витрат, ви можете використовувати бібліотеку діапазонів, яка має ледачі діапазони , наприклад Boost.Range , наприклад:

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

Як зазначає @hvd у коментарі, transform_ifподвійний результат - інший тип ( doubleу даному випадку). Порядок композиції має значення, і з Boost Range ви також можете написати:

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

що призводить до різної семантики. Це рухає додому суть:

це робить дуже мало сенсу включати std::filter_and_transform, std::transform_and_filter, і std::filter_transform_and_filterт.д. і т.п. в стандартній бібліотеці .

Дивіться зразок 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"));
}

29
Ну, проблема в тому, що стандартні алгоритми не можуть бути легко складені, оскільки вони не ліниві.
Ян Гудек,

1
@JanHudec Справді. (Вибач за це? :)). Ось чому ви використовуєте бібліотеку (подібно до того, як ви використовуєте AMP / TBB для одночасності або реактивні розширення в C #). Багато людей працюють над пропозицією асортименту + реалізацією для включення до стандарту.
sehe

2
@sehe +1 Дуже вражаюче, сьогодні я дізнався щось нове! Не могли б ви сказати нам, хто не знайомий з Boost.Range і Phoenix, де ми можемо знайти документацію / приклади, що пояснює, як використовувати boost::phoenixтакі гарні предикати без лямбда? Швидкий пошук у Google не дав нічого актуального. Дякую!
Алі

1
Я не згоден з частиною "дуже мало сенсу включати std :: filter_and_transform". Інші мови програмування також надають цю комбінацію у своїй "стандартній бібліотеці". Повністю має сенс один раз переглядати список елементів, трансформуючи їх на льоту, одночасно пропускаючи ті, які неможливо перетворити. Інші підходи вимагають більше одного проходу. Так, ви можете використовувати BOOST, але насправді питання було: "Чому в стандартній бібліотеці C ++ немає transform_if?". І ІМХО, він має рацію поставити це під сумнів. Така функція повинна бути в стандартній бібліотеці.
Джонні Ді

1
@sehe Щодо "всі вони використовують складаються абстракції": це неправда. Наприклад, у іржі є саме така transform_if. Це називається filter_map. Однак я повинен визнати, що це є для спрощення коду, але, з іншого боку, можна застосувати той самий аргумент у випадку C ++.
Джонні Ді

6

Нове позначення циклу 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 алгоритми? (І в тому випадку, якщо компілятор міг застосувати предикат до будь-якого кінця перетворення, користувач міг легко помилково вибрати неправильний алгоритм, а код все одно скомпілювати, але давати неправильні результати).

Крім того, якщо колекції великі, чи хоче користувач взаємодіяти з ітераторами або зіставляти / зменшувати? З введенням карти / зменшення ви отримуєте ще більше складностей у рівнянні.

По суті, бібліотека надає інструменти, і користувачеві залишається тут використовувати їх відповідно до того, що вони хочуть робити, а не навпаки, як це часто траплялося з алгоритмами. (Подивіться, як користувач вище намагався скрутити речі за допомогою накопичення, щоб відповідати тому, що він насправді хотів зробити).

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

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?


4
Якщо ви вважаєте, що в моєму коді вище є більше місця для помилок, ніж у transform_if з двома лямбдами, однією для предиката, а другою для перетворення, то, будь ласка, поясніть це. Асамблея, C та C ++ - це різні мови та різні місця. Єдине місце, де алгоритм може мати перевагу над циклом, - це можливість "зіставити / зменшити", тому запускати одночасно над великими колекціями. Однак таким чином користувач може контролювати, чи робити цикл послідовно або зменшувати карту.
CashCow

3
У належному функціональному підході функції для предиката та мутатора - це чітко визначені блоки, які роблять структуру належним чином структурованою. Бо тіло циклу може мати в собі довільні речі, і кожен цикл, який ви бачите, повинен бути ретельно проаналізований, щоб зрозуміти його поведінку.
Bartek Banachewicz

2
Залиште належний функціональний підхід для належних функціональних мов. Це C ++.
CashCow

3
"Фантастично вписувати це в алгоритм transform_if?" Те є «transform_if алгоритм», за винятком того, що є всі зашиті.
Р. Мартіньо Фернандес

2
Він виконує еквівалент transform_if. Тільки те, що алгоритми повинні спростити ваш код чи вдосконалити його якимось чином, а не ускладнювати.
CashCow

5

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

1
Не вимірюється - поки користувачі не скаржаться, що їх досвід пов'язаний з процесором (тобто ніколи), мене більше турбує правильність, ніж наносекунди. Однак я не бачу, що це бідно. Необов'язкові кошти дуже дешеві, оскільки немає розподілу пам'яті, і конструктор Ts викликається лише в тому випадку, якщо необов'язковий насправді заповнений. Я очікував би, що оптимізатор усуне майже весь мертвий код, оскільки всі шляхи коду видно під час компіляції.
Річард Ходжес

Ага. Я б погодився, якби мова йшла не зовсім про загальний алгоритм (насправді, загальний будівельний блок всередині них). Це місце, де я зазвичай не захоплений, якщо щось не настільки просто, наскільки це стає. Крім того, я хотів би, щоб необов’язкова обробка була декоратором для будь-якого ітератора виводу (таким чином, принаймні, ми отримуємо можливість компонування вихідних ітераторів, тоді як ми намагаємось усунути відсутність композиції алгоритмів).
sehe

Логічно немає різниці, чи обробляти ви додаткову вставку за допомогою декоратора на ітераторі або у функції перетворення. Зрештою, це просто перевірка прапора. Я думаю, ви виявите, що в будь-якому випадку оптимізований код буде однаковим. Єдине, що заважає повній оптимізації, - це обробка винятків. Позначення Т як відсутності конструкторів, крім інших, це вилікує.
Річард Ходжес

яку форму ви хотіли б прийняти? Я впевнений, що ми могли б створити набір ітераторів, який можна скласти.
Річард Ходжес

Я теж :) Я коментував вашу пропозицію. Я не пропонував щось інше (у мене це було давно. Давайте матимемо замість них діапазони та складові алгоритми :))
sehe

3

Стандарт розроблений таким чином, щоб мінімізувати дублювання.

У цьому конкретному випадку ви можете досягти цілей алгоритму більш читабельним та стислим способом за допомогою простого циклу діапазону.

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

3

Просто знайшовши це питання знову через деякий час, і розробивши цілу низку потенційно корисних універсальних адаптерів-ітераторів, я зрозумів, що оригінальне питання НІЧОГО не вимагало більше 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 

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

4
"Чому ні?" - Тому що код для людей. Для мене тертя насправді гірше, ніж повернення до написання функціональних об'єктів замість лямбда. *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;є нечитабельним і непотрібним конкретним. Дивіться цей c ++ 17 взяти з більш загальними звичаями.
sehe

Ось версія не жорстко кодує базовий ітератор (щоб ви могли використовувати його з std::insert_iterator<>або, std::ostream_iterator<>наприклад), а також дозвольте вам надати перетворення (наприклад, як лямбда). c ++ 17, Починаючи виглядати корисним / Те саме в c ++ 11
sehe

Зауважте, на даний момент мало причин для збереження базових ітераторів, і ви можете просто: використовувати будь-яку функцію , зазначивши, що Boost містить кращу реалізацію: boost :: function_output_iterator . Тепер все, що залишилось - це повторне вигадування for_each_if:)
sehe

Власне, перечитуючи оригінальне запитання, додамо голос розуму - використовуючи лише стандартну бібліотеку 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;
}

Використання: (Зверніть увагу, що 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
    }
);

Ви б оцінили готову продукцію цього впровадження? Чи буде це добре працювати з елементами, які не можна скопіювати? Або ітератори переміщення?
sehe

0

Це лише відповідь на запитання 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;
}

Зауважте, що я щойно реалізував підхід Руста в C ++ тут.


0

Ви можете використовувати, std::accumulateякий працює на вказівник на контейнер призначення:

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 
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.