使用基于范围的for循环,有没有一种方法可以迭代最多N个元素?

44

是否有一种好的方式使用基于范围的for循环和/或标准库中的算法迭代最多N个容器元素(这就是全部意义,我知道可以使用“旧”的for循环加条件来解决)。

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

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

8
“at most N elements” 的意思是“最多不超过N个元素”,请问 @DavidHaim 不理解其中哪部分? - Borgleader
1
@DavidHaim 这意味着如果容器的大小小于或等于N,我希望遍历所有元素;如果容器的大小大于N,则只遍历N个元素。 - declapp auto
5
将“at most N”翻译为代码即:c.size() < N ? c.size() : N,意思是如果集合c的大小小于N,则返回集合c的大小,否则返回N。 - Borgleader
1
@DavidHaim:也许您可以进一步解释一下您的困惑,因为目标明确而清晰地说明了,而且其他人似乎都理解了! - Lightness Races in Orbit
2
使用 range-v3,可以实现如下代码:for (auto&& e: input | ranges::view::take(N)) { /* stuff */ } - T.C.
显示剩余2条评论
11个回答

39

就我个人而言,我会使用这个或者这个答案(两个都点赞),仅仅是为了增加你的知识——你可以使用boost适配器。对于你的情况,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) - 因此更安全(但速度较慢)的版本如下:

一个重要的提示:在sliced中,N需要小于distance(range) - 因此更安全(但速度较慢)的版本如下:

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

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


1
这真是太棒了!Boost库是否也有一种数组视图,可以根据谓词或基于某个索引列表仅给出与之匹配的元素? - Baum mit Augen
1
@BaummitAugen - 当然有 - 看看 boost::adaptors::filtered。但是对于“索引视图”- 可能没有(我不确定)... - PiotrNycz
1
附注:我并不确定它会慢很多——一个具有高优化级别的好编译器应该能够生成类似的二进制文件... - PiotrNycz
1
@BaummitAugen 在你的评论几天后,我遇到了一个需要使用你提到的索引视图的实际问题 - 我设法找到了这样的索引视图解决方案 - 所以我以问答形式在SO上发布了:https://dev59.com/wIzda4cB1Zd3GeqPjjgr - PiotrNycz

14

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

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

这可能会使范围运行近乎两次,但我看不到其他获取范围长度的方法。


2
我建议使用std::advance(begin, N)而不是std::next。如果可用,前者可以利用RandomAccessInterator,而后者则不能。 - Cory Kramer
2
@BaummitAugen 看起来我说谎了,根据标准 § 24.4.4.6 中对于 std::next() 的描述 *"Effects: Equivalent to advance(x, n); return x;"*,我不确定是否有必要利用 RandomAccessIterator,但如果不这样做的话,那就太可惜了。 - Cory Kramer
1
仍然比其他替代方案慢两倍。更不用说可读性差了。 - No-Bugs Hare
1
@LightnessRacesinOrbit 我使用 std::next 是因为我想要给定迭代器的第 n 个后继,这正是 std::next 的作用。 - Baum mit Augen
1
这可能会在范围内运行近两次:对于InputIterator(例如std::cin)而言,这是一个相当棘手的问题。 - Matthieu M.
显示剩余3条评论

8

在需要时,您可以使用经典的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;
    }
}

2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Lightness Races in Orbit
1
在我看来,这个解决方案比 .begin() 和 .end() 更好。更容易阅读、理解和编写代码。 - Support Ukraine
@LightnessRacesinOrbit,我认为在这种情况下,OP应该更详细地澄清他的请求。就个人而言,我将问题视为“从编码点来说最简单的方法是什么”:就像基于范围的循环替换了具有迭代器的等效循环一样,OP可能希望使他的代码尽可能清晰。无论如何,我的答案符合当前措辞的问题。 - Petr
@Petr:我不同意,理由如下。 - Lightness Races in Orbit
+1 "基于范围的for循环和/或标准库中现有的算法"不需要std::算法,我喜欢这里的简洁性。库有些过度,就像用大锤打苍蝇一样,当你已经有适当的拍子时。 - Grault

7

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

实时示例

上述代码显然缺乏一些错误检查和其他调整,但我只想清楚地表达这个想法。

这是因为基于范围的for循环生成类似以下代码的代码

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

cfr. begin_expr and end_expr


4
你的代码不合法,“range(a)”是一个临时变量,“till()”函数返回对它的引用,这个引用会在基于范围的for循环中被绑定(“auto && __range = range_expression”)。然后在执行循环之前,该表达式中的中间临时变量将被删除 - 这样你就得到了一个悬空引用。 - Daniel Frey
@DanielFrey 你说得对。感谢指出。已修复。 - Marco A.

6

如果您的容器没有(或可能没有)RandomAccessIterator,仍然有一种方法可以解决这个问题:

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

对我来说,它非常易读 :-). 而且不论容器类型,它的复杂度为O(N)。


7
问题明确表示他已经知道如何使用自己的for循环来实现这一点。他想要改编标准算法,比如std::for_each。这可能涉及到对迭代器进行调整。 - Lightness Races in Orbit

5
这是一个索引迭代器。大多数都是样板代码,因为我很懒,所以省略了它。
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 )) {
}

现在我们有了一个大型的音乐器材。

实时示例

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

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()[]进行边界检查。

以上代码中可能存在错误,您应该考虑使用boost

如果在indexT上实现-[],甚至可以级联这些范围。


3
自从C++20,您可以将范围适配器 std::views::takeRanges 库 添加到您的 基于范围的 for 循环 中。这样,您就可以实现类似于 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,完整的向量将被打印出来。

Wandbox 上的代码


2

这个解决方案不会超过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时存在相同的问题。 - Matthieu M.
@MatthieuM。从技术上讲,这使他的解决方案与我的相同,因为我的解决方案是早期发布的。无论如何,他的解决方案还提供了一个包装器,用于使用基于范围的for循环,因此它们不相同。此外,除非我错误地解释了boost文档,否则boost解决方案也无法使用InputIterator,因为它需要RandomAccessRange - Alexander Revo

2

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

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

1
你的equal方法让我感到困扰。我理解为什么要使用||,但是我可以想到循环迭代器的问题(例如)。我建议只在那里引用index_,而不必担心迭代器。另外(小事一桩),不要使用int作为index_,最好使用像size_t这样的类型,因为例如int可能只有16位。 - Matthieu M.
我同意使用 size_t。 - ysdx
如果您不比较迭代器,那么如果原始范围中的元素数量低于我们要求的数量,代码将会出错。 - ysdx
1
确实。但是 || this->it_ == other.it_ 似乎不是正确的解决方案,因为它会破坏循环迭代器(是的,C++中的迭代器对概念使事情变得更加困难,一个单独的对象会太容易)。我想知道 Boost 适配器中的 sliced 是否处理循环迭代器。 - Matthieu M.
是的,必须使用一对外部迭代器使得这个东西比它应该的更难。然而,关于这段代码在循环迭代器方面会产生什么问题,我不太确定。 - ysdx

0
自从Ranges成为C++的一部分(在C++20中),实现这一目标的方法是使用“视图适配器”:
   container | std::views::take(N)

作为一个独立的范围,我们可以在for循环中使用它,或将其传递给算法。
例如,问题中的Python循环可以表示为
   for (auto const& i: arr | std::views::take(N)) {
        std::cout << i << '\n';
   }

或者,不使用显式循环:
   using value_type = typename decltype(arr)::value_type;
   auto print = std::ostream_iterator<value_type>{std::cout, '\n'};

   std::ranges::copy(arr | std::views::take(N), print);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接