繁体   English   中英

如何使用 C++ 避免带有“if”条件的“for”循环?

[英]How can I avoid “for” loops with an “if” condition inside them with C++?

对于我编写的几乎所有代码,我经常处理集合上的集合减少问题,这些问题最终以它们内部的幼稚“if”条件结束。 这是一个简单的例子:

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++ 中使用这种首选技术。 有人可以启发我吗?

恕我直言,使用带有 if 的 for 循环更直接,更易读。 但是,如果这对您来说很烦人,您可以使用如下所示的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;
  }
}

用例:

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

for仍然有一个if ,但是从上下文中可以清楚它的作用,除非查找发生更改(例如更改为map ),否则无需更改此代码,并且立即清楚create_object()被调用只有一次,因为它不在循环内。

这是一个快速相对最小的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也有可用的过滤器范围/迭代器。 boost 也有帮助程序,可以让上面的代码写得更短。

一种使用得足以提及但尚未提及的样式是:

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

  DoStuff();
}

优点:

  • 不改变DoStuff();的缩进级别DoStuff(); 当条件复杂度增加时。 逻辑上, DoStuff(); 应该在for循环的顶层,它是。
  • 立即明确循环遍历集合的SOMETHING ,而不需要读者验证if块的关闭}之后没有任何内容。
  • 不需要任何库或帮助宏或函数。

缺点:

  • continue ,像其他的流程控制语句,的方式,导致难以跟踪代码,以至于有些人反对任何利用这些被误用:有编码,一些后续的一个有效的风格,避免continue ,避免在switch之外的break ,避免在函数末尾以外的return
for(auto const &x: myCollection) if(x == something) doStuff();

对我来说,它看起来很像一个特定for C++ 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 是进行人口计数的任何函数( count number of bits = 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)但它有一些明显的缺陷,例如,你必须记住使用逗号代替循环中的分号,并且不能在ac使用逗号运算符。

#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。 这次我们将当前 i 存储在缓冲区中,以便我们可以在需要时在 doStuff() 调用中使用它。

可以将您的代码模式描述为将某个函数应用于某个范围的子集,或者换句话说:将其应用于将过滤器应用于整个范围的结果。

这可以通过 Eric Neibler 的range-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++20 中进入 C++ 标准。

我只想提一下 Mike Acton,他肯定会说:

如果您必须这样做,那么您的数据就有问题。 整理您的数据!

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM