Как я могу избежать циклов "для" с условием "if" внутри них с помощью С++?

С почти всем кодом, который я пишу, я часто сталкиваюсь с задачами набора сокращений в коллекциях, которые в конечном итоге заканчиваются наивными условиями "если" внутри них. Вот простой пример:

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

С функциональными языками я могу решить проблему, уменьшив коллекцию до другой коллекции (легко), а затем выполнив все операции над моим уменьшенным набором. В псевдокоде:

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

И в других вариантах C, таких как С#, я мог бы уменьшить с предложением where, как

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

Или лучше (по крайней мере, на моих глазах)

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

По общему признанию, я делаю много смешения парадигмы и субъективного/основанного на мнениях стиля, но я не могу не чувствовать, что мне не хватает чего-то действительно фундаментального, что позволило бы мне использовать эту предпочтительную технику с С++. Может ли кто-нибудь просветить меня?

Ответ 1

ИМХО это более прямолинейно и более читаемо, чтобы использовать цикл for с if, если внутри него. Однако, если это вас раздражает, вы можете использовать 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; });

Live Demo

Ответ 2

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

Ответ 3

Вместо того, чтобы создавать новый алгоритм, в соответствии с принятым ответом, вы можете использовать существующий с функцией, которая применяет условие:

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

Идея избежать

for(...)
    if(...)
Конструкция

поскольку антипаттерн слишком широк.

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

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

значительно предпочтительнее

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

Он становится антипаттерном, когда будет соответствовать только один элемент, потому что тогда было бы яснее сначала искать элемент и выполнять обработку вне цикла.

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

является крайним и очевидным примером этого. Более тонким и, следовательно, более распространенным, является шаблон factory, например

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

В for по-прежнему существует if, но из контекста становится ясно, что он делает, нет необходимости изменять этот код, если изменения поиска (например, до map), и это сразу видно, что create_object() вызывается только один раз, потому что он не находится внутри цикла.

Ответ 5

Вот быстрая относительно минимальная функция 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(:) -qualifying псевдо-лабиринты, которые я сделал.

В пункте использования это выглядит так:

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

Живой пример.

Существует предлагаемое дополнение к С++ под названием Rangesv3, которое делает такие вещи и многое другое. boost также имеет диапазоны фильтров/итераторы. boost также имеет помощников, которые делают запись намного короче.

Ответ 6

Один стиль, который используется достаточно, чтобы упомянуть, но еще не упоминался, заключается в следующем:

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

  DoStuff();
}

Преимущества:

  • Не меняет уровень отступов DoStuff();, когда сложность условия возрастает. Логически, DoStuff(); должен находиться на верхнем уровне цикла for, и он есть.
  • Сразу же ясно, что цикл повторяется через SOMETHING коллекции, не требуя от читателя проверить, что после закрытия } блока if нет ничего.
  • Не требует каких-либо библиотек или вспомогательных макросов или функций.

Недостатки:

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

Ответ 7

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

Выглядит очень похоже на С++-специфическое понимание for для меня. Вам?

Ответ 8

Если 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);

а затем

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

Ответ 9

Кроме того, если вам не нужно переупорядочивать коллекцию, то std:: partition дешево.

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

Ответ 10

Я испытываю страх перед сложностью вышеупомянутых решений. Я собирался предложить простой #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;
}

Ответ 11

Другое решение в случае важности i: s. Это создает список, который заполняет индексы для вызова doStuff() for. Еще раз главное - избегать ветвления и торговать им за возможные арифметические издержки.

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. На этот раз мы будем иметь текущий i, хранящийся в буфере, чтобы мы могли использовать его в вызове doStuff(), если нам понадобится.