Як я можу уникнути циклів "for" із умовою "if" всередині них за допомогою C ++?


111

Практично з усім кодом, який я пишу, я часто стикаюся з проблемами зменшення кількості колекцій, які в кінцевому підсумку закінчуються наївними умовами "якщо" всередині них. Ось простий приклад:

for(int i=0; i<myCollection.size(); i++)
{
     if (myCollection[i] == SOMETHING)
     {
           DoStuff();
     }
}

За допомогою функціональних мов я можу вирішити проблему, зменшивши колекцію до іншої колекції (легко), а потім виконати всі операції зі скороченого набору. У псевдокоді:

newCollection <- myCollection where <x=true
map DoStuff newCollection

І в інших варіантах C, як-от C #, я міг би скоротити за допомогою пункту, де, як

foreach (var x in myCollection.Where(c=> c == SOMETHING)) 
{
   DoStuff();
}

Або краще (принаймні, на мої очі)

myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));

Справді, я багато змішую парадигми та стиль, заснований на суб'єктивній / думці, але не можу не відчути, що я пропускаю щось дійсно фундаментальне, яке могло б дозволити мені використовувати цю переважну техніку з C ++. Може хтось мене просвітить?


7
Із стандартних функціональних можливостей бібліотеки C ++ ви можете спробувати std::copy_if, але вибір не лінивий
milleniumbug

14
Можливо, вас зацікавить range-v3 . Він також повинен надходити на C ++ як TS і, сподіваємось, стандартизований у майбутньому випуску.
NathanOliver

12
Я відчуваю необхідність зазначити, що ifвсередині forвас згадується не тільки функціонально еквівалентно іншим прикладам, але й, ймовірно, буде швидше у багатьох випадках. Також для тих, хто стверджує, що любить функціональний стиль, те, що ви рекламуєте, схоже, суперечить дорого улюбленій концепції чистоти функціонального програмування, оскільки, DoStuffочевидно, має побічні ефекти.
Фарап

60
Я ніколи не розумів, чому люди, пов’язуючи поєднання всієї логіки на одній лінії, роблять це виглядати якось краще або читабельніше. Ваш фрагмент C ++ на самому верху є на сьогодні найбільш читаним для мене з усіх ваших можливостей. Оскільки ефективність не буде змінена, я не можу зрозуміти, чому ви вважаєте за краще не писати цього, якщо вам не заплатять кількість рядків коду, які ви видаляєте.
Коді Грей

10
@CodyGray Погодився: це просто синтаксичний цукор. І заголовок питання вводить в оману, оскільки він дуже різний, уникаючи розгалуження та приховування його під абстракцією.
edmz

Відповіді:


99

IMHO - це прямо прямо вперед і легше читати використовувати цикл для циклу, якщо всередині нього. Однак якщо це вас дратує, ви можете скористатися for_each_ifподібним нижче:

template<typename Iter, typename Pred, typename Op> 
void for_each_if(Iter first, Iter last, Pred p, Op op) {
  while(first != last) {
    if (p(*first)) op(*first);
    ++first;
  }
}

Usecase:

std::vector<int> v {10, 2, 10, 3};
for_each_if(v.begin(), v.end(), [](int i){ return i > 5; }, [](int &i){ ++i; });

Демонстраційна демонстрація


10
Це винятково розумно. Я також погоджуюся, що це не прямо, і я, мабуть, просто використовуватиму, якщо умови програмування C ++, які споживаються іншими. Але це саме те, що мені потрібно для особистого використання! :)
Darkenor

14
@ За замовчуванням Проходження пар ітераторів, а не контейнерів, є більш гнучким та ідіоматичним C ++.
Марк Б

8
@Slava, загалом діапазони не зменшать кількість алгоритмів. Наприклад, вам все одно потрібно find_ifі findчи працюють вони на діапазонах або парах ітераторів. (Є кілька винятків, таких як for_eachі for_each_n). Спосіб уникнути написання нових альго для кожного чхання - це використовувати різні операції з існуючими альго, наприклад, замість того, щоб for_each_ifвставити умову в передаваний for_each, наприклад,for_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Джонатан Уейклі

9
Мені доведеться погодитися з першим реченням: Стандартне рішення for-if набагато легше читати і легше працювати. Я думаю, що синтаксис лямбда та використання шаблону, визначеного десь ще для обробки простого циклу, буде дратувати або, можливо, заплутувати інших розробників. Ви жертвуєте місцевістю та продуктивністю заради ... чого? Вміти щось написати в одному рядку?
користувач1354557

45
Кашель @Darkenor, як правило , « виключно розумне» програмування слід уникати , бо це дратує лайно з усіх інших , включаючи ваше майбутнє себе.
Райан

48

Boost забезпечує діапазони, для яких можна використовувати з урахуванням діапазону. Діапазони мають ту перевагу , що вони не копіюють , що лежать в основі структури даних, вони просто забезпечують «вид» (тобто begin(), end()для діапазону і operator++(), operator==()для ітератора). Це може вас зацікавити: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.html

#include <boost/range/adaptor/filtered.hpp>
#include <iostream>
#include <vector>

struct is_even
{
    bool operator()( int x ) const { return x % 2 == 0; }
};

int main(int argc, const char* argv[])
{
    using namespace boost::adaptors;

    std::vector<int> myCollection{1,2,3,4,5,6,7,8,9};

    for( int i: myCollection | filtered( is_even() ) )
    {
        std::cout << i;
    }
}

1
Чи можу я замість цього запропонувати використовувати приклад ОП, тобто is_even=> condition, input=> myCollectionтощо
Типово

Це досить відмінна відповідь і, безумовно, що я прагну зробити. Я буду відмовлятися від прийняття, якщо хтось не може придумати стандартний сумісний спосіб зробити це, який використовує ледаче / відкладене виконання. Отримано.
Darkenor

5
@Darkenor: Якщо Boost - це проблема для вас (наприклад, вам заборонено використовувати її через політику компанії та мудрість менеджера), я можу придумати filtered()для вас спрощене визначення - що сказано, краще використовувати підтримується lib, ніж якийсь спеціальний код.
lorro

Повністю згоден з вами. Я прийняв це тому, що стандарт, який відповідає стандарту, став першим, тому що питання було спрямоване саме на C ++, а не на бібліотеку підвищення. Але це справді чудово. Також - так, я, на жаль, працював у багатьох місцях, які заборонили Boost з абсурдних причин ...
Darkenor

@LeeClagett:? .
lorro

44

Замість створення нового алгоритму, як це прийнято, можна використовувати існуючий з функцією, яка застосовує умову:

std::for_each(first, last, [](auto&& x){ if (cond(x)) { ... } });

Або якщо ви дійсно хочете нового алгоритму, принаймні повторно використовуйте for_eachйого замість дублювання логіки ітерації:

template<typename Iter, typename Pred, typename Op> 
  void
  for_each_if(Iter first, Iter last, Pred p, Op op) {
    std::for_each(first, last, [&](auto& x) { if (p(x)) op(x); });
  }

Набагато краще і зрозуміліше для використання стандартної бібліотеки.
анонімний

4
Тому що std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });абсолютно простіше, ніж for (Iter x = first; x != last; x++) if (p(x)) op(x);}?
користувач253751

2
@immibis повторне використання стандартної бібліотеки має інші переваги, такі як перевірка дійсності ітератора, або (у C ++ 17) набагато простіше паралелізувати, просто додавши ще один аргумент: std::for_each(std::execution::par, first, last, ...);Наскільки легко додати ці речі до рукописного циклу?
Джонатан Уейклі

1
#pragma omp паралельно для
Марк К

2
@mark Вибачте, якась випадкова химерність вашого вихідного коду або ланцюжка побудови зробила, що дратує тендітне паралельне нестандартне розширення компілятора генерує нульове підвищення продуктивності без діагностики.
Якк - Адам Невраумон

21

Ідея уникати

for(...)
    if(...)

конструкції як антипатерн занадто широкі.

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

for(...)
    if(...)
        do_process(...);

є переважно перевагою

for(...)
    maybe_process(...);

Це стає антипаттерном, коли буде відповідати лише один елемент, оскільки тоді було б зрозуміліше спочатку шукати елемент і виконувати обробку поза циклом.

for(int i = 0; i < size; ++i)
    if(i == 5)

є крайнім і очевидним прикладом цього. Більш тонким і, таким чином, більш поширеним, є такий фабричний зразок

for(creator &c : creators)
    if(c.name == requested_name)
    {
        unique_ptr<object> obj = c.create_object();
        obj.owner = this;
        return std::move(obj);
    }

Це важко читати, оскільки не очевидно, що код тіла буде виконаний лише один раз. У цьому випадку краще буде відокремити пошук:

creator &lookup(string const &requested_name)
{
    for(creator &c : creators)
        if(c.name == requested_name)
            return c;
}

creator &c = lookup(requested_name);
unique_ptr obj = c.create_object();

Все ще є ifвсередині a for, але з контексту стає зрозуміло, що він робить, не потрібно змінювати цей код, якщо пошук не зміниться (наприклад, на a map), і відразу зрозуміло, що create_object()викликається лише один раз, тому що це не всередині петлі.


Мені це подобається як продуманий і врівноважений огляд, навіть якщо він у певному сенсі відмовляється відповідати на поставлене питання. Я вважаю, що for( range ){ if( condition ){ action } }-style дозволяє легко читати речі за один раз і використовує лише знання основних мовних конструкцій.
PJTraill

@PJTraill, те, як це питання було сформульовано, нагадало мені про те, що Реймонд Чен пробував проти антипатрійну , який був вантажопідйомним і якимось чином став абсолютом. Я повністю погоджуюся, що for(...) if(...) { ... }часто це найкращий вибір (саме тому я кваліфікував рекомендацію розділити дію на підпрограму).
Саймон Ріхтер

1
Дякую за посилання, яке роз’яснило для мене речі: назва « for-if » вводить в оману, і має бути чимось на кшталт « for-all-if-one » або « lookup-избеження ». Це нагадує мені те, як інверсія абстракції була описана Вікіпедією у 2005 році, як коли " створюються прості конструкції поверх складних (тих)", - поки я не переписав її! Насправді я б навіть не поспішав виправляти форму пошуку-процесу-виходу, for(…)if(…)…якби це було єдиним місцем пошуку.
PJTraill

17

Ось швидка відносно мінімальна filterфункція.

Він бере присудок. Він повертає об'єкт функції, який приймає ітерабельний.

Він повертає ітерабельний, який можна використовувати в for(:)циклі.

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  bool empty() const { return begin()==end(); }
};
template<class It>
range_t<It> range( It b, It e ) { return {std::move(b), std::move(e)}; }

template<class It, class F>
struct filter_helper:range_t<It> {
  F f;
  void advance() {
    while(true) {
      (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      if (this->empty())
        return;
      if (f(*this->begin()))
        return;
    }
  }
  filter_helper(range_t<It> r, F fin):
    range_t<It>(r), f(std::move(fin))
  {
      while(true)
      {
          if (this->empty()) return;
          if (f(*this->begin())) return;
          (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      }
  }
};

template<class It, class F>
struct filter_psuedo_iterator {
  using iterator_category=std::input_iterator_tag;
  filter_helper<It, F>* helper = nullptr;
  bool m_is_end = true;
  bool is_end() const {
    return m_is_end || !helper || helper->empty();
  }

  void operator++() {
    helper->advance();
  }
  typename std::iterator_traits<It>::reference
  operator*() const {
    return *(helper->begin());
  }
  It base() const {
      if (!helper) return {};
      if (is_end()) return helper->end();
      return helper->begin();
  }
  friend bool operator==(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    if (lhs.is_end() && rhs.is_end()) return true;
    if (lhs.is_end() || rhs.is_end()) return false;
    return lhs.helper->begin() == rhs.helper->begin();
  }
  friend bool operator!=(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    return !(lhs==rhs);
  }
};
template<class It, class F>
struct filter_range:
  private filter_helper<It, F>,
  range_t<filter_psuedo_iterator<It, F>>
{
  using helper=filter_helper<It, F>;
  using range=range_t<filter_psuedo_iterator<It, F>>;

  using range::begin; using range::end; using range::empty;

  filter_range( range_t<It> r, F f ):
    helper{{r}, std::forward<F>(f)},
    range{ {this, false}, {this, true} }
  {}
};

template<class F>
auto filter( F&& f ) {
    return [f=std::forward<F>(f)](auto&& r)
    {
        using std::begin; using std::end;
        using iterator = decltype(begin(r));
        return filter_range<iterator, std::decay_t<decltype(f)>>{
            range(begin(r), end(r)), f
        };
    };
};

Я взяв скорочення. У справжній бібліотеці повинні бути справжні ітератори, а не for(:)псевдофасади, які я робив.

У момент використання це виглядає приблизно так:

int main()
{
  std::vector<int> test = {1,2,3,4,5};
  for( auto i: filter([](auto x){return x%2;})( test ) )
    std::cout << i << '\n';
}

що досить приємно, і відбитки

1
3
5

Живий приклад .

Існує запропоноване доповнення до C ++ під назвою Rangesv3, яке робить подібне і багато іншого. boostтакож доступні діапазони / ітератори фільтрів. також є помічники, які роблять написання вищезазначених значно коротшим.


15

Один із стилів, який досить звик згадувати, але про нього ще не згадували, це:

for(int i=0; i<myCollection.size(); i++) {
  if (myCollection[i] != SOMETHING)
    continue;

  DoStuff();
}

Переваги:

  • Не змінює рівень відступу DoStuff();при збільшенні складності умови. За логікою, він DoStuff();повинен бути на верхньому рівні forциклу, і він є.
  • Відразу стає ясно , що цикл перебирає SOMETHINGз колекцією, не вимагаючи від читача , щоб переконатися , що немає нічого після закриття }цього ifблоку.
  • Не потрібні бібліотеки чи допоміжні макроси чи функції.

Недоліки:

  • continueяк і інші заяви управління потоком, неправильно використовуються способами, що призводять до важкодотримуваного коду настільки, що деякі люди проти будь-якого їх використання: існує дійсний стиль кодування, який слідкує за тим continue, що деякі уникають , і уникає breakіншого, ніж в a switch, що уникає returnіншого, ніж в кінці функції.

3
Я заперечую, що у forциклі, який працює на багато рядків, дворядковий "якщо ні, то продовжувати" набагато чіткіше, логічніше і читабельніше. Відразу скажіть, «пропустіть це, якщо» після того, як forтвердження читається добре, і, як ви вже сказали, не відступає від інших функціональних аспектів циклу. Якщо continueподальше значення знижується, проте деяка чіткість приносять у жертву (тобто якщо перед ifоператором завжди буде виконуватися деяка операція ).
анонімний

11
for(auto const &x: myCollection) if(x == something) doStuff();

Схоже, схоже на forрозуміння C ++ . Тобі?


Я не думаю, що ключове слово auto було присутнім раніше c ++ 11, тому я б не сказав, що це дуже класичний c ++. Якщо я можу поставити запитання тут у коментарі, чи сказав би "auto const" компілятору, що він може переставляти всі елементи так, як хоче? Можливо, компілятору буде легше планувати уникнути розгалуження, якщо це так.
mathreadler

1
@mathreadler Чим раніше люди перестануть турбуватися про "класичний c ++", тим краще. C ++ 11 був макроеволюційною подією для мови, і йому вже 5 років: це повинен бути мінімум, до якого ми прагнемо. У всякому разі, ОП позначив це і C ++ 14 (ще краще!). Ні, auto constне має жодного відношення до порядку ітерації. Якщо ви подивіться на варіювалися основою for, ви побачите , що це в основному робить стандартний цикл від begin()до end()передбачає разименованія. Ні в якому разі це не може порушити гарантії замовлення (якщо такі є) на повторений контейнер; це було б сміялося з обличчя Землі
підкреслюй_d

1
@mathreadler, насправді це було, це просто мало інше значення. Те, що не було, - це діапазон для ... та будь-яка інша особлива функція C ++ 11. Я мав на увазі тут те, що діапазони fors, std::futures, std::functions, навіть ці анонімні закриття дуже добре C ++ ідуть у синтаксисі; у кожної мови є своя мова, і коли вона містить нові функції, вона намагається змусити їх імітувати старий відомий синтаксис.
bipll

@underscore_d, компілятору дозволено виконувати будь-які перетворення за умови дотримання правила as-if, чи не так?
bipll

1
Хммм, і що, можливо, мається на увазі під цим?
bipll

7

Якщо DoStuff () буде залежати від я якось у майбутньому, тоді я запропонував би цей гарантований варіант безгалузевого бітування маскування.

unsigned int times = 0;
const int kSize = sizeof(unsigned int)*8;
for(int i = 0; i < myCollection.size()/kSize; i++){
  unsigned int mask = 0;
  for (int j = 0; j<kSize; j++){
    mask |= (myCollection[i*kSize+j]==SOMETHING) << j;
  }
  times+=popcount(mask);
}

for(int i=0;i<times;i++)
   DoStuff();

Де popcount - це будь-яка функція, що робить кількість населення (підрахунок кількості біт = 1). Буде певна свобода поставити більш складні обмеження щодо я та їх сусідів. Якщо це не потрібно, ми можемо зняти внутрішню петлю і переробити зовнішню петлю

for(int i = 0; i < myCollection.size(); i++)
  times += (myCollection[i]==SOMETHING);

за ним a

for(int i=0;i<times;i++)
   DoStuff();

6

Крім того, якщо ви не переймаєтесь упорядкуванням колекції, std :: розділ дешевий.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

void DoStuff(int i)
{
    std::cout << i << '\n';
}

int main()
{
    using namespace std::placeholders;

    std::vector<int> v {1, 2, 5, 0, 9, 5, 5};
    const int SOMETHING = 5;

    std::for_each(v.begin(),
                  std::partition(v.begin(), v.end(),
                                 std::bind(std::equal_to<int> {}, _1, SOMETHING)), // some condition
                  DoStuff); // action
}

Але std::partitionпереробляє контейнер.
celtschk

5

Я в захваті від складності вищезазначених рішень. Я збирався запропонувати простий, #define foreach(a,b,c,d) for(a; b; c)if(d)але він має кілька очевидних дефіцитів, наприклад, ви повинні пам’ятати, що в колах замість крапки з комою використовується кома, і ви не можете використовувати оператор кома в aабо c.

#include <list>
#include <iostream>

using namespace std; 

#define foreach(a,b,c,d) for(a; b; c)if(d)

int main(){
  list<int> a;

  for(int i=0; i<10; i++)
    a.push_back(i);

  for(auto i=a.begin(); i!=a.end(); i++)
    if((*i)&1)
      cout << *i << ' ';
  cout << endl;

  foreach(auto i=a.begin(), i!=a.end(), i++, (*i)&1)
    cout << *i << ' ';
  cout << endl;

  return 0;
}

3
Складність деяких відповідей лише висока, оскільки вони спочатку показують багаторазовий загальний метод (який ви зробили б лише один раз), а потім використовуєте його. Неефективно, якщо у вашій програмі є один цикл із умовою if, але дуже ефективний, якщо це відбувається тисячу разів.
gnasher729

1
Як і більшість пропозицій, це ускладнює, а не простіше визначити діапазон та умови вибору. А використання макросу збільшує невизначеність, коли (і як часто) оцінюються вирази, навіть якщо сюрпризів тут немає.
PJTraill

2

Ще одне рішення у випадку, коли i: s є важливими. Цей створює список, який заповнює індекси, для яких потрібно викликати doStuff () для. Ще раз головний момент - уникнути розгалуження та торгувати ним за трубопровідні арифметичні витрати.

int buffer[someSafeSize];
int cnt = 0; // counter to keep track where we are in list.
for( int i = 0; i < container.size(); i++ ){
   int lDecision = (container[i] == SOMETHING);
   buffer[cnt] = lDecision*i + (1-lDecision)*buffer[cnt];
   cnt += lDecision;
}

for( int i=0; i<cnt; i++ )
   doStuff(buffer[i]); // now we could pass the index or a pointer as an argument.

"Чарівна" лінія - це лінія завантаження буфера, яка арифметично обчислює більше, щоб зберегти значення і залишитися в положенні, або підрахувати позицію і додати значення. Таким чином, ми торгуємо потенційною гілкою для деяких логік та арифметики, а може бути, і деяких кеш-хітів. Типовий сценарій, коли це було б корисно, це якщо doStuff () проводить невелику кількість обчислень, що передаються в конвеєр, і будь-яка гілка між дзвінками може перервати ці трубопроводи.

Потім просто переведіть петлю на буфер і запустіть doStuff (), поки ми не досягнемо cnt. Цього разу у нас буде збережений поточний я в буфері, щоб ми могли використовувати його у виклику doStuff (), якщо нам буде потрібно.


1

Можна описати ваш шаблон коду як застосування якоїсь функції до підмножини діапазону, або іншими словами: застосувати його до результату застосування фільтра до всього діапазону.

Цього можна досягти найпростішим способом за допомогою бібліотеки діапазонів-v3 Еріка Нейблера ; хоча це трохи зору, тому що ви хочете працювати з індексами:

using namespace ranges;
auto mycollection_has_something = 
    [&](std::size_t i) { return myCollection[i] == SOMETHING };
auto filtered_view = 
    views::iota(std::size_t{0}, myCollection.size()) | 
    views::filter(mycollection_has_something);
for (auto i : filtered_view) { DoStuff(); }

Але якщо ви готові відмовитися від індексів, ви отримаєте:

auto is_something = [&SOMETHING](const decltype(SOMETHING)& x) { return x == SOMETHING };
auto filtered_collection = myCollection | views::filter(is_something);
for (const auto& x : filtered_collection) { DoStuff(); }

що приємніше ІМХО.

PS - Бібліотека діапазонів здебільшого переходить у стандарт C ++ у C ++ 20.


0

Я згадаю лише Майка Актона, він би точно сказав:

Якщо вам доведеться це зробити, у вас є проблеми зі своїми даними. Сортуйте свої дані!

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