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



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



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


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



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


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



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


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



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


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

660   11  

11 ответов:

IMHO более прямолинейно и более читаемо использовать цикл 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; });

Демо

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

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

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

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

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 внутри for, но из контекста становится ясно, что он делает, нет необходимости изменять этот код, если поиск не изменится (например, на map), и сразу понятно, что create_object() вызывается только один раз, потому что это не внутри цикла.

вот быстрый относительно минимальный .

он принимает предикат. Он возвращает объект функции, который принимает итератор.

он возвращает итерацию, которая может быть использована в 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 также имеются диапазоны фильтров / итераторы. boost также имеет помощников, которые делают написание выше намного короче.

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

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

  DoStuff();
}

плюсы:

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

недостатки:

  • continue, как и другие операторы управления потоком, неправильно используется таким образом, что приводит к трудно следовать коду так много, что некоторые люди выступают против любой использование их: существует допустимый стиль кодирования, который некоторые следуют, что позволяет избежать continue, чтобы избежать break кроме switch, что избегает return кроме как в конце функции.
for(auto const &x: myCollection) if(x == something) doStuff();

выглядит в значительной степени как C++-specific for понимание ко мне. Для тебя?

Если DoStuff () будет зависеть от i каким-то образом в будущем, то я бы предложил этот гарантированный вариант без ветвей битовой маскировки.

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 ). Там будет некоторая свобода, чтобы поставить более продвинутые ограничения с i и их соседями. Если это не нужно, мы можем снять внутренний цикл и переделать внешний цикл

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

затем

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

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

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

другое решение в случае, если 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 (), если нам это понадобится.

Comments

    Ничего не найдено.