如何使用C ++避免在其中使用“if”条件的“for”循环?

时间:2016-07-15 14:59:55

标签: c++ c++11 c++14

对于我写的几乎所有代码,我经常处理集合上的集合减少问题,最终最终会在其中出现天真的“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 ++中使用这种首选技术。有人可以启发我吗?

13 个答案:

答案 0 :(得分:97)

恕我直言,使用带有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;
  }
}

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

答案 1 :(得分:48)

Boost提供可以基于范围使用的范围。范围的优势在于它们不复制基础数据结构,它们仅提供“视图”(即begin()end()用于范围和operator++(),{{1对于迭代器来说)。这可能是您感兴趣的:http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.html

operator==()

答案 2 :(得分:42)

您可以使用具有应用条件的函数的现有算法,而不是像接受的答案那样创建新算法:

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

答案 3 :(得分: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内仍有for,但从上下文中可以清楚地了解它的作用,除非查找发生更改,否则无需更改此代码(例如{{1}很明显map只被调用一次,因为它不在循环内。

答案 4 :(得分: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

Live example

有一个名为Rangesv3的C ++新增功能可以做到这一点。 boost也有可用的过滤范围/迭代器。提升也有帮助,使上述写作更短。

答案 5 :(得分:15)

一种足以提及但尚未提及的风格是:

func vkSdkAccessAuthorizationFinishedWithResult(result: VKAuthorizationResult!){

        let tokenString = result.token.description
        FIRAuth.auth()?.signInWithCustomToken(tokenString) { (user, error) in
            // ...

        }
    }

优点:

  • 当条件复杂性增加时,不会更改ALTER TABLE your_db.your_table MODIFY Your_enum_col ENUM('new_name_1','new_name_2','new_name_3'); 的缩进级别。从逻辑上讲,for(int i=0; i<myCollection.size(); i++) { if (myCollection[i] != SOMETHING) continue; DoStuff(); } 应该位于DoStuff();循环的顶层,而且它是。
  • 立即清楚地表明循环遍历集合的DoStuff();,而不要求读者在for块的结束SOMETHING之后验证没有任何内容。
  • 不需要任何库或帮助程序宏或函数。

缺点:

    与其他流量控制语句一样,
  • }被滥用的方式导致难以理解的代码,以至于某些人反对任何使用它们:一种有效的编码风格,有些遵循避免if,避免除continue之外的continue,除了在函数末尾之外避免break。< / LI>

答案 6 :(得分:11)

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

看起来非常像C ++特有的for对我的理解。给你?

答案 7 :(得分: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();

答案 8 :(得分: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
}

答案 9 :(得分:5)

我对上述解决方案的复杂性感到敬畏。我打算建议一个简单的#define foreach(a,b,c,d) for(a; b; c)if(d),但它有一些明显的缺陷,例如,你必须记住在循环中使用逗号而不是分号,并且你不能在{{1中使用逗号运算符}或a

c

答案 10 :(得分: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()时使用它。如果需要的话。

答案 11 :(得分:1)

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

这可以通过Eric Neibler的ranges-v3 library以最直接的方式实现。尽管有点麻烦,但是因为您要使用索引:

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 ++标准。

答案 12 :(得分:0)

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

如果必须这样做,则数据有问题。对数据进行排序!