C++11中的基于范围的for循环

10

在C++11中,如果我们有一个set<int> S,我们可以这样说:

for (auto i: S)
    cout << i << endl;

但是我们能否强制 i 成为一个迭代器,我的意思是编写一段等效于以下代码的代码:

for (auto i = S.begin(); i != S.end(); i++)
    cout << (i != s.begin()) ? " " : "" << *i;

我们能否做些什么使得我们能够了解i在集合(或向量)中的索引位置呢?

还有一个问题是,我们如何表达不要对S中的所有元素执行此操作,而是只对前一半或除第一个元素外的所有元素执行此操作。

或者当我们有一个vector<int> V时,想要打印它的前n个值,我们应该怎么做?我知道我们可以创建一个新的向量,但复制一个向量到新向量需要时间。


我在这里问了一个类似的问题:http://stackoverflow.com/questions/8960403/get-first-n-elements-in-a-multiset(针对multiset而言) - XCS
1
打印 vector 的前 n 个值:for (auto val : vec | sliced(0,n)) {...}。请参阅 sliced, from Boost.Range - Luc Touraille
1
range-based for 的目的是为了更轻松地迭代整个范围。其目的并不是完全替代非 range for。有些情况下你需要使用常规的 for;range for 只是一种常见迭代模式的语法糖。它不能覆盖所有情况,也不应该这样做。 - Nicol Bolas
@LucTouraille:但它不在C++11标准中。 - Farzam
在我看来,你最好使用std::copyinfix_ostream_iterator - Jerry Coffin
6个回答

20

很不幸,标准是这样规定的:

基于范围的for语句 for ( for-range-declaration : expression ) statement 等价于

{
    auto && __range = ( expression );
    for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin ) {
        for-range-declaration = *__begin;
        statement
    }
}
换句话说,它已经从“begin”迭代到“end”并且已经解引用了迭代器,你永远无法看到它。

有趣的是,这意味着__begin和__end必须是相同类型。 - bcmpinc
@bcmpinc:但它们是否可以是不同类型的?您只能迭代数组或模板容器(好吧,从技术上讲,您可以迭代任何具有::begin()和::end()重载的内容...但您知道我的意思),因此所有元素,包括__begin和__end,无论如何都是相同的类型。它们可能是指向派生类的指针,是的...但即使其中一个指针指向“不同的东西”,它也是相同的类型。 - Damon
2
使用过滤前向迭代器(遍历集合,但仅访问满足谓词的值)更容易使迭代器检查是否完成(它必须这样做),而不是将其与结束迭代器进行比较。通过定义end()返回一些不同的虚拟类型,可以实现operator!=,以便它仅检查迭代器是否已完成。(我最近实现了这样的迭代器。) - bcmpinc

9

基于范围的 for 的原则是对整个范围进行迭代。

然而,决定范围是什么,因此可以对范围本身进行操作。

template <typename It>
class RangeView {
public:
  typedef It iterator;

  RangeView(): _begin(), _end() {}
  RangeView(iterator begin, iterator end): _begin(begin), _end(end) {}

  iterator begin() const { return _begin; }
  iterator end() const { return _end; }

private:
  iterator _begin;
  iterator _end;
};

template <typename C>
RangeView<typename C::iterator> rangeView(C& c, size_t begin, size_t end) {
  return RangeView<typename C::iterator>(
           std::next(c.begin(), begin),
           std::next(c.begin(), end)
         );
}

template <typename C>
RangeView<typename C::const_iterator> rangeView(C const& c, size_t begin, size_t end) {
  return RangeView<typename C::const_iterator>(
           std::next(c.begin(), begin),
           std::next(c.begin(), end)
         );
}

好的,这个看起来很像 Boost.Range...

现在,让我们使用它!

for (auto i: rangeView(set, 1, 10)) {
  // iterate through the second to the ninth element
}

@Xeo:我希望我们有更多这样的视图 :) 不幸的是,有些视图很简单(就像这个),但“转换”视图可能会变得非常复杂。它们适合于具有不可变性的函数式语言,但“引用”思想在转换上实现起来比必要的困难(绑定到临时问题...)。那样迭代器很快就会变得臃肿。 - Matthieu M.

2

对于一般情况,您需要使用单独的变量:

int i = 0;

for (auto x : s)
    cout << (i++ ? " " : "") << x << endl;

当然,对于某些容器(如vector),有一些技巧可用,但并非适用于所有容器。

对于此目的,您最好使用普通的for循环。


2
不可以。
for (... : ...)

之所以使用for而不是foreach,只是为了不引入新的关键字。 foreach的整个意义在于快速简洁地迭代所有元素,而不需要关心它们的索引。 对于其他所有情况,都有简单的for可以有效地完成其任务。


2

在set中你不能这样做。请使用传统的for语法或维护自己的索引计数器。

在vector或其他具有平面布局的容器中,如std::array或C风格数组,你可以这样做。将其更改为使用引用。

for (auto &i: S)

然后,您可以将i的地址与s [0]的地址进行比较,以获取索引。


你为什么不用 std::set 做同样的事情呢? for(auto &i: S) { if (&i == &*S.begin()) { - MSalters
@MSalters:我认为OP是在询问任何i的索引,例如检测i是否在集合的“前半部分”。您是正确的,可以测试索引0的特殊情况。 - Drew Dormann
我理解这是特别指第一个元素,因为意图是在N个元素之间打印N-1个分隔符。 - MSalters
@MSalters:问题似乎在一半的时候改变了焦点。 - Drew Dormann

1

基于范围的 for 语句适用于简单情况。我期望它在原型设计时可能会有些用处,但在实际成为产品之前,它的使用大多数情况下都已经消失了。它可能对初学者有所帮助,但这是我无法判断的领域(但似乎驱动了很多最近的 C++ 讨论)。

唯一比较有建设性的方法可能是使用一个引用底层范围并调整迭代器的 begin()end() 方法的适配器。还要注意,您可能希望将任何特殊处理第一个或最后一个元素的操作提升到处理大量数据的循环之外。当然,这只是另一个检查,后面跟着一个正确预测的分支,而不是没有检查和更少的分支预测表污染。


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