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

前端 未结 13 1690
滥情空心
滥情空心 2021-01-30 03:31

With almost all code I write, I am often dealing with set reduction problems on collections that ultimately end up with naive \"if\" conditions inside of them. Here\'s a simple

相关标签:
13条回答
  • 2021-01-30 04:03

    Instead of creating a new algorithm, as the accepted answer does, you can use an existing one with a function that applies the condition:

    std::for_each(first, last, [](auto&& x){ if (cond(x)) { ... } });
    

    Or if you really want a new algorithm, at least reuse for_each there instead of duplicating the iteration logic:

    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); });
      }
    
    0 讨论(0)
  • 2021-01-30 04:05

    Boost provides ranges that can be used w/ range-based for. Ranges have the advantage that they don't copy the underlying data structure, they merely provide a 'view' (that is, begin(), end() for the range and operator++(), operator==() for the iterator). This might be of your interest: 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;
        }
    }
    
    0 讨论(0)
  • 2021-01-30 04:07

    One can describe your code pattern as applying some function to a subset of a range, or in other words: applying it to the result of applying a filter to the whole range.

    This is achievable in the most straightforward manner with Eric Neibler's ranges-v3 library; although it's a bit of an eyesore, because you want to work with indices:

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

    But if you're willing to forego indices, you'd get:

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

    which is nicer IMHO.

    PS - The ranges library is mostly going into the C++ standard in C++20.

    0 讨论(0)
  • 2021-01-30 04:09

    Here is a quick relatively minimal filter function.

    It takes a predicate. It returns a function object that takes an iterable.

    It returns an iterable that can be used in a for(:) loop.

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

    I took short cuts. A real library should make real iterators, not the for(:)-qualifying pseudo-fascades I did.

    At point of use, it looks like this:

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

    which is pretty nice, and prints

    1
    3
    5
    

    Live example.

    There is a proposed addition to C++ called Rangesv3 which does this kind of thing and more. boost also has filter ranges/iterators available. boost also has helpers that make writing the above much shorter.

    0 讨论(0)
  • 2021-01-30 04:11

    I am in awe of the complexity of the above solutions. I was going to suggest a simple #define foreach(a,b,c,d) for(a; b; c)if(d) but it has a few obvious deficits, for example, you have to remember to use commas instead of semicolons in your loop, and you can't use the comma operator in a or 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;
    }
    
    0 讨论(0)
  • 2021-01-30 04:13

    The idea of avoiding

    for(...)
        if(...)
    

    constructs as an antipattern is too broad.

    It is completely fine to process multiple items that match a certain expression from inside a loop, and the code cannot get much clearer than that. If the processing grows too large to fit on screen, that is a good reason to use a subroutine, but still the conditional is best placed inside the loop, i.e.

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

    is vastly preferable to

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

    It becomes an antipattern when only one element will match, because then it would be clearer to first search for the element, and perform the processing outside of the loop.

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

    is an extreme and obvious example of this. More subtle, and thus more common, is a factory pattern like

    for(creator &c : creators)
        if(c.name == requested_name)
        {
            unique_ptr<object> obj = c.create_object();
            obj.owner = this;
            return std::move(obj);
        }
    

    This is hard to read, because it isn't obvious that the body code will be executed once only. In this case, it would be better to separate the lookup:

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

    There is still an if within a for, but from the context it becomes clear what it does, there is no need to change this code unless the lookup changes (e.g. to a map), and it is immediately clear that create_object() is called only once, because it is not inside a loop.

    0 讨论(0)
提交回复
热议问题