【问题标题】:Is there a way to iterate over at most N elements using range-based for loop?有没有办法使用基于范围的 for 循环迭代最多 N 个元素?
【发布时间】:2015-08-27 05:00:44
【问题描述】:

有没有一种好方法可以使用基于范围的for 循环和/或标准库中的算法迭代容器中最多 N 个元素(这就是重点,我知道我可以只使用“旧" for 带条件循环)。

基本上,我正在寻找与此 Python 代码相对应的东西:

for i in arr[:N]:
    print(i)

【问题讨论】:

  • @DavidHaim “最多 N 个元素”到底有什么令人困惑的地方?
  • @DavidHaim 这意味着如果容器的大小小于或等于 N,我想遍历容器中的所有元素,否则遍历 N 个元素。
  • @DavidHaim "最多 N" -> c.size() < N ? c.size() : N
  • @DavidHaim:也许你可以进一步解释你的困惑,因为目标很明确,而且其他人似乎都明白了!

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


【解决方案1】:

我个人会使用thisthis 答案(两者都+1),只是为了增加您的知识-您可以使用增强适配器。对于您的情况 - sliced 似乎是最合适的:

#include <boost/range/adaptor/sliced.hpp>
#include <vector>
#include <iostream>

int main(int argc, const char* argv[])
{
    std::vector<int> input={1,2,3,4,5,6,7,8,9};
    const int N = 4;
    using boost::adaptors::sliced;
    for (auto&& e: input | sliced(0, N))
        std::cout << e << std::endl;
}

一个重要说明:sliced 要求 N 不大于 distance(range) - 所以更安全(和更慢)的版本如下:

    for (auto&& e: input | sliced(0, std::min(N, input.size())))

所以 - 再一次 - 我会使用更简单的旧 C/C++ 方法(这是你想在你的问题中避免的;)

【讨论】:

  • 这真是太棒了! Boost 是否也有某种数组视图,可以只给我匹配谓词或基于某个索引列表的元素?
  • @BaummitAugen - 肯定有 - 看看boost::adaptors::filtered。但是对于“索引视图” - 可能不是(我不确定)......
  • 旁注:我不确定它“慢得多” - 具有高优化级别的良好编译器应该能够生成类似的二进制文件......
  • @BaummitAugen 在您发表评论几天后,我遇到了一个现实世界的问题,需要您提到的索引视图 - 我设法找到了这样的索引视图解决方案 - 所以我在 SO Q/A 格式:stackoverflow.com/questions/30976131/…
【解决方案2】:

这是我能想到的适用于所有前向迭代器的最便宜的保存解决方案:

auto begin = std::begin(range);
auto end = std::end(range);
if (std::distance(begin, end) > N)
    end = std::next(begin,N);

这可能会通过该范围几乎两次,但我认为没有其他方法可以获取该范围的长度。

【讨论】:

  • 我建议使用std::advance(begin, N) 而不是std::next。前者可能会利用RandomAccessInterator,如果可用,后者不会。
  • @BaummitAugen 看起来我在撒谎,从标准的§ 24.4.4.6 换成std::next() "效果:相当于advance(x, n); return x;" I'我不确定利用 RandomAccessIterator 是否是要求,但如果他们不这样做,那就太可惜了。
  • 仍然比替代品慢两倍。更不用说可读性差了。
  • @LightnessRacesinOrbit 我使用了std::next,因为我想要给定迭代器的第n 个后继,这正是std::next 的用途。
  • 这可能会在该范围内运行几乎两次:对于 InputIterator 来说是一个相当棘手的问题(例如 std::cin)。
【解决方案3】:

您可以在需要时使用旧的break 手动中断循环。它甚至适用于基于范围的循环。

#include <vector>
#include <iostream>

int main() {
    std::vector<int> a{2, 3, 4, 5, 6};
    int cnt = 0;
    int n = 3;
    for (int x: a) {
       if (cnt++ >= n) break;
       std::cout << x << std::endl;
    }
}

【讨论】:

  • -1:问题明确指出他已经知道如何使用自己的 for 循环来执行此操作。我意识到他也要求提供远程想法,但是您的建议确实没有添加任何特定于远程的想法。他想调整标准算法,例如std::for_each。这可能会涉及到迭代器。
  • 在我看来,这个解决方案比 .begin() 和 .end() 的东西要好。更容易阅读、理解和编码。
  • @LightnessRacesinOrbit,我认为在这种情况下,OP 应该更详细地澄清他的要求。就我个人而言,我将这个问题视为“从编码的角度来看最简单的方法是什么”:就像基于范围的循环用迭代器替换了等效循环一样,OP 可能希望使他的代码尽可能清晰。无论如何,我的回答与当前措辞中的问题相符。
  • @Petr:出于给出的原因,我不同意。
  • +1 “标准库中基于范围的 for 和/或算法”不需要 std:: 算法,我喜欢这里的简单性。图书馆是矫枉过正的,就像你有一个适当的苍蝇拍的大锤。
【解决方案4】:

C++ 很棒,因为您可以编写自己的可怕 解决方案并将它们隐藏在抽象层下

#include <vector>
#include <iostream>

//~-~-~-~-~-~-~- abstraction begins here ~-~-~-~-~-//
struct range {
 range(std::vector<int>& cnt) : m_container(cnt),
   m_end(cnt.end()) {}
 range& till(int N) {
     if (N >= m_container.size())
         m_end = m_container.end();
     else
        m_end = m_container.begin() + N;
     return *this;
 }
 std::vector<int>& m_container;
 std::vector<int>::iterator m_end;
 std::vector<int>::iterator begin() {
    return m_container.begin();
 }
 std::vector<int>::iterator end() {
    return m_end;
 }
};
//~-~-~-~-~-~-~- abstraction ends here ~-~-~-~-~-//

int main() {
    std::vector<int> a{11, 22, 33, 44, 55};
    int n = 4;

    range subRange(a);        
    for ( int i : subRange.till(n) ) {
       std::cout << i << std::endl; // prints 11, then 22, then 33, then 44
    }
}

Live Example

上面的代码显然缺少一些错误检查和其他调整,但我只是想表达清楚。

这是因为range-based for loops 产生类似于以下的代码

{
  auto && __range = range_expression ; 
  for (auto __begin = begin_expr,
       __end = end_expr; 
       __begin != __end; ++__begin) { 
    range_declaration = *__begin; 
    loop_statement 
  } 
} 

cfr。 begin_exprend_expr

【讨论】:

  • 你的代码是非法的,range(a) 是一个临时的,till() 返回一个对它的引用并且该引用被绑定在基于范围的 for 循环中 (auto &amp;&amp; __range = range_expression)。然后在执行循环之前删除表达式中的中间临时对象 - 您最终会得到一个悬空引用。
  • @DanielFrey 你是对的。感谢您指出了这一点。固定。
【解决方案5】:

如果您的容器没有(或可能没有)RandomAccessIterator,仍然有办法给这只猫剥皮:

int cnt = 0;
for(auto it=container.begin(); it != container.end() && cnt < N ; ++it,++cnt) {
  //
}

至少对我来说,它的可读性很强:-)。无论容器类型如何,它都有 O(N) 复杂度。

【讨论】:

  • -1:问题明确指出他已经知道如何使用自己的 for 循环来执行此操作。他想调整标准算法,例如std::for_each。这可能会涉及到迭代器。
【解决方案6】:

这是一个索引迭代器。主要是样板文件,省略了,因为我很懒。

template<class T>
struct indexT
 //: std::iterator< /* ... */ > // or do your own typedefs, or don't bother
{
  T t = {};
  indexT()=default;
  indexT(T tin):t(tin){}
  indexT& operator++(){ ++t; return *this; }
  indexT operator++(int){ auto tmp = *this; ++t; return tmp; }
  T operator*()const{return t;}
  bool operator==( indexT const& o )const{ return t==o.t; }
  bool operator!=( indexT const& o )const{ return t!=o.t; }
  // etc if you want full functionality.
  // The above is enough for a `for(:)` range-loop
};

它包装了一个标量类型T,并在* 上返回一个副本。有趣的是,它也适用于迭代器,这在这里很有用,因为它可以让我们有效地从指针继承:

template<class ItA, class ItB>
struct indexing_iterator:indexT<ItA> {
  ItB b;
  // TODO: add the typedefs required for an iterator here
  // that are going to be different than indexT<ItA>, like value_type
  // and reference etc.  (for simple use, not needed)
  indexing_iterator(ItA a, ItB bin):ItA(a), b(bin) {}
  indexT<ItA>& a() { return *this; }
  indexT<ItA> const& a() const { return *this; }
  decltype(auto) operator*() {
    return b[**a()];
  }
  decltype(auto) operator->() {
    return std::addressof(b[**a()]);
  }
};

索引迭代器包含两个迭代器,其中第二个必须是随机访问的。它使用第一个迭代器来获取一个索引,并使用它从第二个迭代器中查找一个值。

接下来,我们有一个范围类型。在很多地方都可以找到经过 SFINAE 改进的版本。它使得在 for(:) 循环中迭代一系列迭代器变得容易:

template<class Iterator>
struct range {
  Iterator b = {};
  Iterator e = {};
  Iterator begin() { return b; }
  Iterator end() { return e; }
  range(Iterator s, Iterator f):b(s),e(f) {}
  range(Iterator s, size_t n):b(s), e(s+n) {}
  range()=default;
  decltype(auto) operator[](size_t N) { return b[N]; }
  decltype(auto) operator[] (size_t N) const { return b[N]; }\
  decltype(auto) front() { return *b; }
  decltype(auto) back() { return *std::prev(e); }
  bool empty() const { return begin()==end(); }
  size_t size() const { return end()-begin(); }
};

以下是帮助轻松处理indexT 范围的助手:

template<class T>
using indexT_range = range<indexT<T>>;
using index = indexT<size_t>;
using index_range = range<index>;

template<class C>
size_t size(C&&c){return c.size();}
template<class T, std::size_t N>
size_t size(T(&)[N]){return N;}

index_range indexes( size_t start, size_t finish ) {
  return {index{start},index{finish}};
}
template<class C>
index_range indexes( C&& c ) {
  return make_indexes( 0, size(c) );
}
index_range intersect( index_range lhs, index_range rhs ) {
  if (lhs.b.t > rhs.e.t || rhs.b.t > lhs.b.t) return {};
  return {index{(std::max)(lhs.b.t, rhs.b.t)}, index{(std::min)(lhs.e.t, rhs.e.t)}};
}

好的,差不多了。

index_filter_it 采用一系列索引和一个随机访问迭代器,并将一系列索引迭代器放入该随机访问迭代器的数据中:

template<class R, class It>
auto index_filter_it( R&& r, It it ) {
  using std::begin; using std::end;
  using ItA = decltype( begin(r) );
  using R = range<indexing_iterator<ItA, It>>;
  return R{{begin(r),it}, {end(r),it}};
}

index_filter 接受一个index_range 和一个随机访问容器,将它们的索引相交,然后调用index_filter_it

template<class C>
auto index_filter( index_range r, C& c ) {
  r = intersect( r, indexes(c) );
  using std::begin;
  return index_filter_it( r, begin(c) );
}

现在我们有了:

for (auto&& i : index_filter( indexes(0,6), arr )) {
}

还有中提琴,我们有一个大型乐器。

live example

更高级的过滤器是可能的。

size_t filter[] = {1,3,0,18,22,2,4};
using std::begin;
for (auto&& i : index_filter_it( filter, begin(arr) ) )

将访问arr 中的 1、3、0、18、22、2、4。但是,它不会进行边界检查,除非arr.begin()[] bounds-checks。

上面的代码可能有错误,你应该使用boost

如果您在indexT 上实现-[],您甚至可以菊花链这些范围。

【讨论】:

    【解决方案7】:

    由于C++20,您可以将范围适配器std::views::takeRanges library 添加到您的range-based for loop。这样您就可以实现与PiotrNycz's answer 中的解决方案类似的解决方案,但无需使用 Boost:

    int main() {
        std::vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9};
        const int N = 4;
    
        for (int i : v | std::views::take(N))
            std::cout << i << std::endl;
            
        return 0;
    }
    

    这个解决方案的好处是N 可能大于向量的大小。这意味着,对于上面的示例,使用N = 13 是安全的;然后将打印完整的矢量。

    Code on Wandbox

    【讨论】:

      【解决方案8】:

      这个解决方案不超过end(),对于std::list 具有O(N) 复杂性(不使用std::distance)适用于std::for_each,并且只需要ForwardIterator

      std::vector<int> vect = {1,2,3,4,5,6,7,8};
      
      auto stop_iter = vect.begin();
      const size_t stop_count = 5;
      
      if(stop_count <= vect.size())
      {
          std::advance(stop_iter, n)
      }
      else
      {
          stop_iter = vect.end();
      }
      
      std::for_each(vect.vegin(), stop_iter, [](auto val){ /* do stuff */ });
      

      它唯一不做的就是使用InputIterator,例如std::istream_iterator - 你必须使用外部计数器。

      【讨论】:

      • 与 Marco A 的提案相同,InputIterator 的问题相同。
      • @MatthieuM。从技术上讲,这将使他的解决方案与我的相同,因为我的解决方案较早发布。无论如何,他的解决方案还提供了一个包装器来使用基于范围的 for 循环,因此它们并不相同。此外,除非我将boost documentation 解释错,否则boost solution 也不能与InputIterator 一起使用,因为它需要RandomAccessRange
      【解决方案9】:

      首先我们编写一个在给定索引处停止的迭代器:

      template<class I>
      class at_most_iterator
        : public boost::iterator_facade<at_most_iterator<I>,
                        typename I::value_type,
                        boost::forward_traversal_tag>
      {
      private:
        I it_;
        int index_;
      public:
        at_most_iterator(I it, int index) : it_(it), index_(index) {}
        at_most_iterator() {}
      private:
        friend class boost::iterator_core_access;
      
        void increment()
        {
          ++it_;
          ++index_;
        }
        bool equal(at_most_iterator const& other) const
        {
          return this->index_ == other.index_ || this->it_ == other.it_;
        }
        typename std::iterator_traits<I>::reference dereference() const
        {
          return *it_;
        }
      };
      

      我们现在可以编写一个算法,让这个迭代器在给定的范围内大放异彩:

      template<class X>
      boost::iterator_range<
        at_most_iterator<typename X::iterator>>
      at_most(int i, X& xs)
      {
        typedef typename X::iterator iterator;
        return std::make_pair(
                  at_most_iterator<iterator>(xs.begin(), 0),
                  at_most_iterator<iterator>(xs.end(), i)
              );
      }
      

      用法:

      int main(int argc, char** argv)
      {
        std::vector<int> xs = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        for(int x : at_most(5, xs))
          std::cout << x << "\n";
        return 0;
      }
      

      【讨论】:

      • 你的equal 方法让我很困扰。我理解为什么您使用||,但是我可以想到循环迭代器的问题(例如)。我建议只在那里引用index_,而根本不打扰迭代器。另外(nit),不要将int 用于index_,更喜欢size_t 之类的东西,因为int 可以小到16 位。
      • 我同意应该使用 size_t。
      • 如果不比较迭代器,如果原始范围内的元素数量低于我们要求的数量,代码就会中断。
      • 确实如此。但是|| this-&gt;it_ == other.it_ 似乎是错误的解决方案,因为它破坏了循环迭代器(是的,C++ 中的迭代器对概念使事情变得更难,单个对象太容易了)。我想知道 Boost 适配器中的 sliced 是否处理循环迭代器。
      • 是的,不得不使用一对外部迭代器让这件事变得比它应该的更难。我不太确定这段代码会破坏什么。然而,一个循环迭代器。
      猜你喜欢
      • 2020-06-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-20
      • 2017-03-20
      • 2011-10-20
      • 2018-08-18
      • 2014-09-13
      • 1970-01-01
      相关资源
      最近更新 更多