C++11反向范围for循环

406

是否存在一种容器适配器,可以反转迭代器的方向,这样我就可以通过range-based for-loop以相反的顺序迭代容器了?

使用显式迭代器,我会将其转换为:

for (auto i = c.begin(); i != c.end(); ++i) { ...

变成这样:

for (auto i = c.rbegin(); i != c.rend(); ++i) { ...

我想将这个转换成:

for (auto& i: c) { ...

变成这个:

for (auto& i: std::magic_reverse_adapter(c)) { ...

有这样的东西吗,或者我必须自己编写?


23
一个反向容器适配器,听起来很有趣,但我认为你需要自己编写它。如果标准委员会能够尽快采用基于范围的算法而不是显式迭代器,我们就不会有这个问题了。 - deft_code
5
为什么要放弃基于迭代器的算法?它们在处理不是从 beginend 迭代或处理流迭代器等情况时更好且更简洁。虽然范围算法很棒,但它们实际上只是迭代器算法的语法糖(除了可能有惰性评估的可能性)。 - Nicol Bolas
20
@deft_code的代码作用是将容器逆序遍历。可以优化该代码(例如添加const版本等),但它已经能够正常工作。以下是示例代码: vector<int> v {1, 2, 3}; reverse_adapter<decltype(v)> ra; for (auto& i : ra) cout << i; 输出结果为 321 - Seth Carnegie
11
@SethCarnegie:再加上一个好用的函数形式:template<typename T> reverse_adapter<T> reverse_adapt_container(T &c) {return reverse_adapter<T>(c);} 这样你就可以使用 for(auto &i: reverse_adapt_container(v)) cout << i; 来迭代了。 - Nicol Bolas
2
@C.R:我认为它不应该意味着那样,因为这会使其在循环的简洁语法中无法使用,而循环顺序确实很重要。在我看来,简洁性比您的语义含义更重要/有用,但如果您不重视简洁性,当然您的风格指南可以赋予它任何含义。如果以某种形式纳入标准,parallel_for就是这种情况下的更强烈的“我不关心顺序”的条件。当然,它也可以具有基于范围的语法糖 :-) - Steve Jessop
显示剩余2条评论
10个回答

281

实际上Boost确实拥有这样的适配器:boost::adaptors::reverse

#include <list>
#include <iostream>
#include <boost/range/adaptor/reversed.hpp>

int main()
{
    std::list<int> x { 2, 3, 5, 7, 11, 13, 17, 19 };
    for (auto i : boost::adaptors::reverse(x))
        std::cout << i << '\n';
    for (auto i : x)
        std::cout << i << '\n';
}

22
我只是厌倦了看到现在各种各样的答案都推销boost。 - Hitesh Kumar Saini

125

实际上,在C++14中可以用非常少的代码完成。

这个想法与@Paul的解决方案非常相似。由于C++11缺少一些东西,那个解决方案有点过度膨胀(而且在std中定义会有味道)。感谢C++14,我们可以使它更加易读。

关键观察是,基于范围的for循环是通过依赖于begin()end()来获取范围的迭代器来工作的。由于ADL,甚至不需要在std命名空间中定义自定义的begin()end()

这里是一个非常简单的示例解决方案:

// -------------------------------------------------------------------
// --- Reversed iterable

template <typename T>
struct reversion_wrapper { T& iterable; };

template <typename T>
auto begin (reversion_wrapper<T> w) { return std::rbegin(w.iterable); }

template <typename T>
auto end (reversion_wrapper<T> w) { return std::rend(w.iterable); }

template <typename T>
reversion_wrapper<T> reverse (T&& iterable) { return { iterable }; }

这真是太棒了,例如:

template <typename T>
void print_iterable (std::ostream& out, const T& iterable)
{
    for (auto&& element: iterable)
        out << element << ',';
    out << '\n';
}

int main (int, char**)
{
    using namespace std;

    // on prvalues
    print_iterable(cout, reverse(initializer_list<int> { 1, 2, 3, 4, }));

    // on const lvalue references
    const list<int> ints_list { 1, 2, 3, 4, };
    for (auto&& el: reverse(ints_list))
        cout << el << ',';
    cout << '\n';

    // on mutable lvalue references
    vector<int> ints_vec { 0, 0, 0, 0, };
    size_t i = 0;
    for (int& el: reverse(ints_vec))
        el += i++;
    print_iterable(cout, ints_vec);
    print_iterable(cout, reverse(ints_vec));

    return 0;
}

正如预期的那样打印

4,3,2,1,
4,3,2,1,
3,2,1,0,
0,1,2,3,

注意:GCC-4.9版本中还未实现std::rbegin()std::rend()std::make_reverse_iterator()。我根据标准编写这些示例,但它们不能在稳定版本的g++中编译。不过,为这三个函数添加临时存根非常容易。下面是一个示例实现,虽然不完整,但对于大多数情况已经足够:

// --------------------------------------------------
template <typename I>
reverse_iterator<I> make_reverse_iterator (I i)
{
    return std::reverse_iterator<I> { i };
}

// --------------------------------------------------
template <typename T>
auto rbegin (T& iterable)
{
    return make_reverse_iterator(iterable.end());
}

template <typename T>
auto rend (T& iterable)
{
    return make_reverse_iterator(iterable.begin());
}

// const container variants

template <typename T>
auto rbegin (const T& iterable)
{
    return make_reverse_iterator(iterable.end());
}

template <typename T>
auto rend (const T& iterable)
{
    return make_reverse_iterator(iterable.begin());
}

82
几行代码?请原谅,这至少有十行以上的代码。 - Jonny
4
实际上,这取决于如何计算行数,可能是5-13行。解决方法不应该存在,因为它们是库的一部分。顺便提一下,感谢您提醒我,这个答案需要更新以适应最近编译器版本,其中所有额外的行都不再需要。 - Prikso NAI
3
我认为你在reverse实现中忘记了使用forward<T> - SnakE
4
如果你将这个放在头文件中,那么你就在头文件中使用using namespace std,这不是一个好主意。或者我漏掉了什么? - estan
5
实际上,在头文件的文件作用域内不应该编写"using <anything>;"。您可以通过将using声明移至begin()和end()函数作用域中来改进上述代码。 - Chris Hartman
显示剩余10条评论

61

我从cppreference获取了这个示例。 它适用于:

GCC 10.1+,带有标志-std=c++20。

#include <ranges>
#include <iostream>
 
int main()
{
    static constexpr auto il = {3, 1, 4, 1, 5, 9};
 
    std::ranges::reverse_view rv {il};
    for (int i : rv)
        std::cout << i << ' ';
 
    std::cout << '\n';
 
    for(int i : il | std::views::reverse)
        std::cout << i << ' ';
}

31

如果您可以使用range v3,那么您可以使用反向范围适配器ranges::view::reverse,它允许您以反向方式查看容器。

一个最小的工作示例:

#include <iostream>
#include <vector>
#include <range/v3/view.hpp>

int main()
{
    std::vector<int> intVec = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    for (auto const& e : ranges::view::reverse(intVec)) {
        std::cout << e << " ";   
    }
    std::cout << std::endl;

    for (auto const& e : intVec) {
        std::cout << e << " ";   
    }
    std::cout << std::endl;
}

请查看DEMO 1

注意:根据Eric Niebler的说法,此功能将在C++20中提供。 这可以与<experimental/ranges/range>头文件一起使用。 然后,for语句将如下所示:

for (auto const& e : view::reverse(intVec)) {
       std::cout << e << " ";   
}

请参见DEMO 2


5
更新:ranges::view 命名空间已更名为 ranges::views。因此,请使用 ranges::views::reverse - nac001

27

这应该在不使用boost的情况下在C++11中工作:

namespace std {
template<class T>
T begin(std::pair<T, T> p)
{
    return p.first;
}
template<class T>
T end(std::pair<T, T> p)
{
    return p.second;
}
}

template<class Iterator>
std::reverse_iterator<Iterator> make_reverse_iterator(Iterator it)
{
    return std::reverse_iterator<Iterator>(it);
}

template<class Range>
std::pair<std::reverse_iterator<decltype(begin(std::declval<Range>()))>, std::reverse_iterator<decltype(begin(std::declval<Range>()))>> make_reverse_range(Range&& r)
{
    return std::make_pair(make_reverse_iterator(begin(r)), make_reverse_iterator(end(r)));
}

for(auto x: make_reverse_range(r))
{
    ...
}

80
据我所知,向命名空间std添加任何内容都是一种引发严重问题的邀请。 - BCS
44
我不确定“epic fail”的规范含义,但是在std命名空间中重载函数会产生未定义的行为,根据17.6.4.2.1。 - Casey
9
似乎是C++14的语法,名称就是这个。 - HostileFork says dont trust SE
7
很不幸,这样做会发生冲突。你不能同时使用两个定义编译。此外,编译器不需要在 C++11 下不存在定义而只出现在 C++14 中(规范没有关于 std:: 命名空间中 不存在 的内容,只有存在的内容)。因此,在符合标准的 C++11 编译器下,这将是一个非常可能的编译错误……比如果它是一些在 C++14 中 不存在 的随机名称要更有可能!正如所指出的,这是“未定义行为”……所以编译失败并不是它可能做的最糟糕的事情。 - HostileFork says dont trust SE
2
@HostileFork,没有名称冲突,make_reverse_iterator不在std命名空间中,因此它不会与C++14版本发生冲突。 - Paul Fultz II
显示剩余3条评论

12

抱歉,除了C++20之外的当前C++版本中,所有这些解决方案似乎都不如使用基于索引的for循环好。这里没有什么是只有“几行代码”的。因此,是的:通过一个简单的int循环来迭代。这是最佳解决方案。


1
索引或简单的 rbegin()/rend() - Alexis Wilke
@AlexisWilke 是的,OP自己提到了,但实际上:同意!特别是对于std::rbegin/std::rend,它甚至可以用于数组。 - IceFire

11

这个对你起作用吗:

#include <iostream>
#include <list>
#include <boost/range/begin.hpp>
#include <boost/range/end.hpp>
#include <boost/range/iterator_range.hpp>

int main(int argc, char* argv[]){

  typedef std::list<int> Nums;
  typedef Nums::iterator NumIt;
  typedef boost::range_reverse_iterator<Nums>::type RevNumIt;
  typedef boost::iterator_range<NumIt> irange_1;
  typedef boost::iterator_range<RevNumIt> irange_2;

  Nums n = {1, 2, 3, 4, 5, 6, 7, 8};
  irange_1 r1 = boost::make_iterator_range( boost::begin(n), boost::end(n) );
  irange_2 r2 = boost::make_iterator_range( boost::end(n), boost::begin(n) );


  // prints: 1 2 3 4 5 6 7 8 
  for(auto e : r1)
    std::cout << e << ' ';

  std::cout << std::endl;

  // prints: 8 7 6 5 4 3 2 1
  for(auto e : r2)
    std::cout << e << ' ';

  std::cout << std::endl;

  return 0;
}

9
template <typename C>
struct reverse_wrapper {

    C & c_;
    reverse_wrapper(C & c) :  c_(c) {}

    typename C::reverse_iterator begin() {return c_.rbegin();}
    typename C::reverse_iterator end() {return c_.rend(); }
};

template <typename C, size_t N>
struct reverse_wrapper< C[N] >{

    C (&c_)[N];
    reverse_wrapper( C(&c)[N] ) : c_(c) {}

    typename std::reverse_iterator<const C *> begin() { return std::rbegin(c_); }
    typename std::reverse_iterator<const C *> end() { return std::rend(c_); }
};


template <typename C>
reverse_wrapper<C> r_wrap(C & c) {
    return reverse_wrapper<C>(c);
}

例如:

int main(int argc, const char * argv[]) {
    std::vector<int> arr{1, 2, 3, 4, 5};
    int arr1[] = {1, 2, 3, 4, 5};

    for (auto i : r_wrap(arr)) {
        printf("%d ", i);
    }
    printf("\n");

    for (auto i : r_wrap(arr1)) {
        printf("%d ", i);
    }
    printf("\n");
    return 0;
}

1
你能否详细解释一下你的答案? - Mostafiz
这是一个基于逆向范围循环的C++11类模板。 - Khan Lau

4
您可以简单地使用BOOST_REVERSE_FOREACH进行反向迭代。例如,代码如下:
#include <iostream>
#include <boost\foreach.hpp>

int main()
{
    int integers[] = { 0, 1, 2, 3, 4 };
    BOOST_REVERSE_FOREACH(auto i, integers)
    {
        std::cout << i << std::endl;
    }
    return 0;
}

生成以下输出:
4

3

2

1

0

1
你是指 <boost/foreach.hpp> 吗? - undefined

2
如果不使用C++14,则我认为以下是最简单的解决方案。
#define METHOD(NAME, ...) auto NAME __VA_ARGS__ -> decltype(m_T.r##NAME) { return m_T.r##NAME; }
template<typename T>
struct Reverse
{
  T& m_T;

  METHOD(begin());
  METHOD(end());
  METHOD(begin(), const);
  METHOD(end(), const);
};
#undef METHOD

template<typename T>
Reverse<T> MakeReverse (T& t) { return Reverse<T>{t}; }

演示

对于没有 begin/rbegin, end/rend 函数的容器/数据类型(例如数组),此方法不起作用。


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