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


111

用我编写的几乎所有代码,我经常要处理集合的集合约简问题,这些集合最终会在集合内部出现幼稚的“ 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 ++一起使用。有人可以启发我吗?


7
您可以尝试使用C ++标准库功能之外的功能,std::copy_if但是选择并不懒惰
milleniumbug

14
您可能对range-v3感兴趣。它也应该以TS的形式出现在C ++中,并有望在将来的版本中进行标准化。
NathanOliver 2016年

12
我觉得有必要指出的是,if一个里面for你提到不仅是非常功能上等同于其他的例子,但也很可能会快于很多情况下。同样对于声称喜欢函数式风格的人来说,您要推广的内容似乎与函数式编程的DoStuff备受推崇的纯净概念背道而驰,因为显然有副作用。
法拉普

60
我从来没有真正理解过人们为什么将所有逻辑组合成一条线,使其看起来更好或更可读。在您的所有可能性中,到目前为止,您最喜欢的C ++代码段对我来说最具可读性。而且由于效率不会改变,所以我不明白为什么您不愿写它,除非您按删除的代码行数付费。
科迪·格雷

10
@CodyGray同意:它只是语法糖。问题标题具有误导性,因为避免分支并将其隐藏在抽象之下非常不同。
edmz

Answers:


99

恕我直言,在其中使用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; });

现场演示


10
那是非常聪明的。我也将同意这不是直接的,我可能只会在编程别人使用的C ++时使用if条件。但这正是我个人使用所需要的!:)
Darkenor

14
@Default传递迭代器对而不是容器是更灵活和惯用的C ++。
Mark B

8
@Slava,通常不会减少算法的数量。例如,您仍然需要find_if以及find它们是否适用于范围或成对的迭代器。(有一些例外,例如for_eachfor_each_n)。避免为每次打喷嚏编写新算法的方法是对现有算法使用不同的操作,例如,而不是for_each_if将条件嵌入传递给的可调用对象中for_each,例如for_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Jonathan Wakely

9
我要与第一句一致认为:该标准,如果解决方案是更容易阅读和使用工作。我认为lambda语法以及在其他地方定义的模板的使用只是为了处理一个简单的循环,这会激怒其他开发人员,或者可能使其他开发人员感到困惑。您要为...牺牲本地性和性能?能够在一行中写东西?
user1354557 '16

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
我建议使用OP的例子相反,即is_even=> conditioninput=> myCollection
默认

这是一个非常好的答案,而且绝对是我想要做的。我将推迟接受,除非有人可以提出一种使用延迟/延迟执行的标准兼容方式来做到这一点。已投票。
Darkenor

5
@Darkenor:如果Boost对您来说是个问题(例如,由于公司政策和经理的智慧,您被禁止使用它),我可以filtered()为您提供一个简化的定义-也就是说,最好使用受支持的库,而不是某些临时代码。
洛罗

完全同意你的看法。我之所以接受它,是因为首先出现的是符合标准的方式,因为问题是针对C ++本身的,而不是针对Boost库的。但这确实很棒。另外-是的,我可悲地曾在很多地方工作,出于荒谬的原因而禁止了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);}
user253751 '16

2
@immibis重用标准库还有其他好处,例如迭代器有效性检查,或者(在C ++ 17中)更容易并行化,只需添加一个参数即可: std::for_each(std::execution::par, first, last, ...);将这些内容添加到手写循环有多容易?
Jonathan Wakely

1
#pragma omp parallel for
马克·科文

2
@mark抱歉,您的源代码或构建链的某些随机变化使烦人的易碎并行非标准编译器扩展产生零性能提升,而没有诊断。
Yakk-Adam Nevraumont

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,但是从上下文中可以清楚地知道它的作用,除非查找发生更改(例如,更改为map),否则无需更改此代码,并且立即可以清楚地create_object()仅调用一次,因为它是不在循环内。


我喜欢这样做,尽管它在某种意义上拒绝回答提出的问题,但它是一个经过深思熟虑且平衡的概述。我发现for( range ){ if( condition ){ action } }-style使得一次读取一个块很容易,并且仅使用基本语言结构的知识。
PJTraill '16

@PJTraill,问题的表达方式使我想起了雷蒙德·陈Raymond Chen)对假想反模式的rant讽,这种反模式已被大量研究,并在某种程度上成为绝对的。我完全同意这for(...) if(...) { ... }通常是最好的选择(这就是为什么我有资格将建议拆分为一个子例程的建议)。
西蒙·里希特

1
感谢您为我澄清的链接:“ for-if ” 的名称具有误导性,应类似于“ for-if-if-one ”或“ look-avoidance ”。它使我想起了Wikipedia在2005年描述抽象反转的方式,当时一个“ 在复杂的(一个)之上创建简单的结构 ” —直到我重写了!实际上,我什至不急于修复lookup-process-exit形式,如果它是唯一发生查找的地方。for(…)if(…)…
PJTraill '16

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(:)我做的-qualified伪门面。

在使用时,它看起来像这样:

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还提供了一些帮助程序,可以使上述内容的编写过程大大缩短。


15

足以被提及但尚未被提及的一种样式是:

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

  DoStuff();
}

优点:

  • DoStuff();条件复杂度增加时,不更改缩进级别。从逻辑上讲,DoStuff();应该位于for循环的顶层。
  • 立即清楚地表明,在循环迭代SOMETHING的集合S,而不需要读者验证是否有闭幕后没有}了的if块。
  • 不需要任何库或辅助宏或函数。

缺点:

  • continue像其他的流程控制语句,得到的方式,导致难以跟踪代码,以至于有些人反对滥用任何使用它们:有编码的一个有效的样式,一些后续避免continue,避免break比其他在中switch,应避免return在函数末尾除外。

3
我认为在一个for循环到多行的循环中,两行“如果没有,继续”则更加清晰,逻辑和可读。在for语句读起来不错之后,立即说“如果是否跳过”,并且正如您所说的,不会缩进循环的其余功能方面。continue但是,如果进一步下降,则会牺牲一些清晰度(即,如果始终在if语句之前执行某些操作)。
匿名

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

在我看来,这很像C ++的for理解。给你?


我认为c ++ 11之前没有auto关键字,所以我不会说它是非常经典的c ++。如果我可以在评论中问一个问题,“ auto const”会告诉编译器它可以根据需要重新排列所有元素吗?如果这样的话,编译器可能会更容易计划避免分支。
mathreadler '16

1
@mathreadler人们越早不再担心“经典c ++”,那就更好了。C ++ 11是该语言的一次宏大进化事件,并且已经有5年历史了:它应该是我们追求的最低标准。无论如何,OP标记了C ++ 14(甚至更好!)。不,auto const与迭代顺序无关。如果您查找基于范围的for,您会发现它基本上通过隐式解引用从begin()end()进行了标准循环。它不可能破坏正在迭代的容器的订购保证(如果有的话)。它会一直笑而不地球表面
underscore_d

1
@mathreadler,实际上是,它的含义完全不同。不存在的范围是...以及任何其他独特的C ++ 11功能。我在这里的意思是,range-for,std::futures,std::functions,甚至那些匿名闭包在语法上也都很好。每种语言都有其自己的说法,并且在引入新功能时会尝试使其模仿旧的众所周知的语法。
bipll

@underscore_d,允许编译器执行任何转换,只要遵守规则即可,不是吗?
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);

跟一个

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

6

另外,如果您不在乎对集合进行重新排序,则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
}

但是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 '16

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


1

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

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


0

我只会提到Mike Acton,他肯定会说:

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.