【问题标题】:Reversing range-based for loop on a custom container class在自定义容器类上反转基于范围的 for 循环
【发布时间】:2017-07-02 10:27:58
【问题描述】:

我正在尝试通过移植 Sedgewick 和 Wayne 的算法,第 4 版中的主要示例来提高我的 C++ 技能。我基于他们的 Java example 编写了一个通用堆栈实现。

我的堆栈工作正常,但我想提高性能,但在尝试编写反向迭代器时遇到了困难。

template<typename T> class ResizingArrayStack {
public:
    T* begin() { return &array_ptr[0]; }
    T* end() { return &array_ptr[N]; }

...

// Here we're iterating forward through the array, with an unused variable `i`.
// It would be nice performance-wise to iterate in reverse without calling pop(), and without triggering a resize.
for ( auto& i : lifo_stack ) {
    cout << "Current loop iteration has i = " << i << endl;
}
// // Alternatively, pop from the stack N times.
// cout << "Popped an item from the stack: " << lifo_stack.pop() << endl;

我尝试切换上面的beginend成员函数,但发现扩展的for循环总是以++__begin递增,即使__end位于较低的内存地址。我们如何让i 反向循环(相对于堆栈的 LIFO)?

如果存在严重错误或看起来过时的方面,请随时评论我的代码风格。我希望与优秀的“现代”C++ 保持一致。

【问题讨论】:

标签: c++ for-loop iterator c++14 reverse-iterator


【解决方案1】:

如果您想使用带有反向迭代器的 range-for 循环,您可以使用包装类 Reverse,它存储一个范围并返回对应于 beginendreverse_iterators

#include <iostream>
#include <iterator>
#include <vector>

template<class Rng>
class Reverse
{
    Rng const& rng;    
public:    
    Reverse(Rng const& r) noexcept
    : 
        rng(r)
    {}

    auto begin() const noexcept { using std::end; return std::make_reverse_iterator(end(rng)); }
    auto end()   const noexcept { using std::begin; return std::make_reverse_iterator(begin(rng)); }
};

int main()
{
    std::vector<int> my_stack;
    my_stack.push_back(1);
    my_stack.push_back(2);
    my_stack.push_back(3);

    // prints 3,2,1
    for (auto const& elem : Reverse(my_stack)) {
        std::cout << elem << ',';    
    }
}

Live Example

注意这里使用C++1z模板推导,仅g++ 7.0 SVN和clang 5.0 SVN支持。对于早期的编译器,您可以添加一个辅助函数

    template<class Rng>
    auto MakeReverse(Rng const& rng) { return Reverse<Rng>(rng); }

    for (auto const& elem : MakeReverse(my_stack)) {
        std::cout << elem << ',';    
    }

Live Example(从 gcc 5.1 或 clang 3.5 开始工作)

或者,您可以使用 Boost.Range library 并简单地执行(适用于任何 C++11 编译器)

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

int main()
{
    std::vector<int> my_stack;
    my_stack.push_back(1);
    my_stack.push_back(2);
    my_stack.push_back(3);

    for (auto const& elem : boost::adaptors::reverse(my_stack)) {
        std::cout << elem << ',';    
    }
}

Live Example

请注意,您必须小心将临时变量传递给此类适配器,我的和 Boost 适配器在传递时都不起作用,例如一个原始的std::vector&lt;int&gt;{3,2,1},正如@Pixelchemist 在 cmets 中指出的那样。

【讨论】:

  • Pre-C++1z,你也可以做std在其他地方做的事情,并引入一个make_reverse函数模板来为你做这些。
  • 1. for(auto i : MakeReverse(std::vector&lt;int&gt;{1,2,3})) { /* land of doom here */ } 又名“不可能有右值”。 2. 仅当std::beginstd::end '工作' 时才有效。如果您需要启用 adl 的 beginend,事情会变得更加复杂。
  • @Pixelchemist 这并不比 Boost.Range 适配器 reverse 差。你让我达到更高的标准吗? :) 不过更新了。
  • 我既不知道也不使用 boost.range 但如果该适配器捕获右值并通过引用保存它们或保存右值的迭代器,那么我认为它有些损坏或至少很危险。 for(auto i : make_vector(1,2,3)){ ... } 完全有效,所以我不希望 for(auto i : reverse(make_vector(1,2,3))){ ... } 导致未定义的行为领域。
  • @Pixelchemist 非常适合提升它,我的简单适配器也是如此,但现在记录在案,ADL 的东西也已修复。
【解决方案2】:

这里是你的问题的划痕。不要将其视为工作代码。使用它来了解如何实现反向迭代器(仅一种可能的方式)。

template<typename T> class ResizingArrayStack {
public: 
    class reverse_iterator
    {       
        ResizingArrayStack & _storage;
        int _pointer;

    public:
        inline reverse_iterator(ResizingArrayStack & storage,
                                int pointer)
            : _storage(storage)
            , _pointer(pointer)
        {}

        inline reverse_iterator & operator++() // prefix
        {
            --_pointer;
            return *this;
        }

        inline reverse_iterator operator++() // postfix
        {
            reverse_iterator tmp(*this);
            --_pointer;
            return tmp;
        }

        inline T & operator*()
        {
            return _storage.getByIndex(_pointer);
        }

        // TODO: == != etc
    };      

    reverse_iterator rbegin() { return reverse_iterator(*this, N - 1); }
    reverse_iterator rend() { return reverse_iterator(*this, -1); }
    // ...  //
};

【讨论】:

    【解决方案3】:

    一旦你有了正常运行的(常规)迭代器, 使用标准库实现反向迭代器 助手类模板std::reverse_iterator

    #include <iterator>
    
    class XX { 
    
        // your code
    
        typedef std::reverse_iterator<iterator> reverse_iterator;
    
        reverse_iterator rbegin() { return reverse_iterator{end()}; }
        reverse_iterator rend() { return reverse_iterator{begin()}; }
    

    【讨论】:

      【解决方案4】:

      查看您的完整代码lifo_stack.pop() 会使您的迭代器无效,因此它不能在范围内使用。您有未定义的行为

      此外,将 range for 用于堆栈没有多大意义。如果您可以迭代其元素,那么它现在就不是堆栈了,不是吗?堆栈具有您可以访问最近插入的元素的属性。


      根据您的评论:

      考虑一下您缓慢而单独地添加项目的情况,但是 希望尽快将它们从堆栈中转储。我不 想要 pop() 复制和调整数组大小的开销 在那一刻触发。

      我仍然认为 ranged-for 对堆栈没有意义。

      这是我认为您的问题已解决的方法:

      lifo_stack.disable_resizing(); // prevents resizing 
      while (!lifo_stack.is_empty()
      {
          lifo_stack.pop(); // maybe use the poped element
      }
      lifo_stack.enable_resizing(); // re-enables resizing and triggers a resize
      

      如果您不需要弹出的元素而只想清空堆栈,有一种更快的方法(基于您的类实现):

      // empties the stack
      void clear()
      {
         delete[] array_ptr;
         array_ptr = new T[1];;
         max_size = 1;
         N = 0;
      }
      

      如果你想使用现代 C++,那么最后一个决赛是使用 unique_ptr 而不是手动 newdelete。它更容易但最重要的是更安全。并阅读 0/3/5 的规则。

      【讨论】:

      • 我认为不需要堆栈意味着您无法访问所有元素。堆栈的主要属性是只能推送和弹出。
      • @Angew "堆栈的主要属性是你只能推送和弹出" 正确。这意味着您只能访问 pop 返回的元素,该元素是堆栈中最近推送的元素。这意味着你不能迭代它的元素(比如在 ranged for 中)。
      • 你说得对,我应该从这个例子中删除 lifo_stack.pop() 。它改变了我正在迭代的数据结构,这是 bad。也就是说,我不认为迭代堆栈是不合理的,只要它以 LIFO 顺序发生。考虑一下您缓慢而单独地添加项目,但希望尽快将它们从堆栈中转储的情况。我不希望在那时触发 pop() 的复制和调整数组大小的开销。堆栈示例大致基于此 (algs4.cs.princeton.edu/13stacks/ResizingArrayStack.java.html)。
      • @bolov 我需要一个“只能在一端插入和删除的可迭代容器”的次数大于我的次数我们需要一个“只能访问最顶层元素的堆栈”。因此,对我来说,可迭代的堆栈是有意义的。我声称堆栈的定义属性是如何修改它,而不是如何读取它。
      • @Angew 这绝对是一个非常有用的容器。然而,它不是一个堆栈。至少在严格的定义中没有。
      【解决方案5】:

      此解决方案不会引入不必要的副本,也不会出现某些 cmets 建议的错误转发。解释如下。

      您可以使用一些具有开始和结束功能的包装器 返回反向迭代器。

      template<class T>
      struct revert_wrapper
      {
          T o;
          revert_wrapper(T&& i) : o(std::forward<T>(i)) {}
      };
      
      template<class T>
      auto begin(revert_wrapper<T>& r)
      {
          using std::end;
          return std::make_reverse_iterator(end(r.o));
      }
      
      template<class T>
      auto end(revert_wrapper<T>& r)
      {
          using std::begin;
          return std::make_reverse_iterator(begin(r.o));
      }
      
      template<class T>
      auto begin(revert_wrapper<T> const& r) 
      { 
          using std::end;
          return std::make_reverse_iterator(end(r.o));
      }
      
      template<class T>
      auto end(revert_wrapper<T> const& r)
      {
          using std::begin;
          return std::make_reverse_iterator(begin(r.o));
      }
      
      template<class T>
      auto reverse(T&& ob)
      {
          return revert_wrapper<T>{ std::forward<T>(ob) };
      }
      

      这样使用:

      std::vector<int> v{1, 2, 3, 4};
      for (auto i : reverse(v))
      {
          std::cout << i << "\n";
      }
      

      或者在你的情况下

      for ( auto& i : reverse(lifo_stack) ) {
          cout << "Current loop iteration has i = " << i << endl;
          cout << "Popped an item from the stack: " << lifo_stack.pop() << endl;
      }
      

      由于转发不是一个简单的话题,并且存在误解,我将进一步解释一些细节。我将使用std::vector&lt;int&gt; 作为“待反转”类型T 的示例。

      1。函数模板reverse

      1.1 传递左值std::vector&lt;int&gt;:

      std::vector<int> v{1, 2, 3, 4};
      auto&& x = reverse(v);
      

      在这种情况下,编译器创建的 reverse 实例如下所示:

      template<>
      auto reverse<std::vector<int>&>(std::vector<int>& ob)
      {
          return revert_wrapper<std::vector<int>&>{ std::forward<std::vector<int>&>(ob) };
      }
      

      我们在这里看到两件事:

      • revert_wrapperT 将是 std::vector&lt;int&gt;&amp;,因此不涉及复制。
      • 我们将左值作为左值转发给revert_wrapper 的构造函数

      1.2 传递右值std::vector&lt;int&gt;

      std::vector<int> foo();
      auto&& x = reverse(foo());
      

      我们再看函数模板的实例化:

      template<>
      auto reverse<std::vector<int>>(std::vector<int>&& ob)
      {
          return revert_wrapper<std::vector<int>>{ std::forward<std::vector<int>>(ob) };
      }
      

      并且可以再次注意两件事:

      • revert_wrapperT 将是 std::vector&lt;int&gt;,因此复制向量,防止右值在任何基于范围的循环运行之前超出范围
      • 右值std::vector&lt;int&gt;&amp;&amp; 将被转发给revert_wrapper 的构造函数

       

      2。类模板revert_wrapper 及其构造函数

      2.1 reverse 在左值std::vector&lt;int&gt;&amp; 的情况下创建的revert_wrapper

      template<>
      struct revert_wrapper<std::vector<int>&>
      {
          std::vector<int>& o;
          revert_wrapper(std::vector<int>& i) : 
              o(std::forward<std::vector<int>&>(i)) {}
      };
      

      如上所述:我们存储参考时不涉及副本。 forward 看起来也很熟悉,实际上它与上面的reverse 相同:我们将左值作为左值引用。

      2.2 reverse 在右值std::vector&lt;int&gt;&amp;&amp; 的情况下创建的revert_wrapper

      template<>
      struct revert_wrapper<std::vector<int>>
      {
          std::vector<int> o;
          revert_wrapper(std::vector<int>&& i) : 
              o(std::forward<std::vector<int>>(i)) {}
      };
      

      这一次我们将对象按值存储以防止悬空引用。 转发也很好:我们将右值引用从reverse 转发到revert_wrapper 构造函数,然后我们将它转​​发到std::vector 构造函数。我们可以以同样的方式使用static_cast&lt;T&amp;&amp;&gt;(i),但我们不是来自左值的(std::)mov(e),而是转发:

      • 左值作为左值和
      • 右值作为右值。

      我们还可以在这里看到另一件事: 按值存储的revert_wrapper 实例的唯一可用构造函数采用右值。因此,我们不能(轻易)欺骗这个类来制作不必要的副本。

      请注意,在 revert_wrapper 构造函数的 o 的初始化程序中将 std::forward 替换为 std::move 实际上是错误的。

      【讨论】:

      • 它接缝reverse() 复制了整个容器
      • @sp2danny:不。如果传递了左值,reverse 的推导转发引用将使 T 成为引用类型。如果传递了一个右值,revert_wrapper 将按值存储对象(这确实有意义,否则引用会悬空)。
      • @TemplateRex: 不。推论是在reverse 中完成的,它创建了正确的模板实例。如果将左值(即U&amp;)传递给reverse,则T= U&amp;revert_wrapper 持有引用。如果将右值 (U&amp;&amp;) 传递给 reverse,则 T = Urevert_wrapper 按值保存项目。
      • 我说的是revert_wrapper构造函数
      • @TemplateRex: std::move 不能在此处使用,因为std::moveT 是左值引用的情况下的结果不是左值引用而是右值引用,这不是我们想要什么。构造函数正在转发给成员。扣除没有发生(而是向上移动调用树),但我们仍然在这里转发。使用std::move 会导致错误,例如“从T 类型的右值初始化T&amp; 类型的非常量引用无效。”
      【解决方案6】:

      请参阅 TemplateRex here 的出色回答。 我能够在没有包装类的情况下解决问题,所以我将尝试回答我自己的问题。

      这是我在http://en.cppreference.com 上找到的关于实现迭代器的最有用的example,您可以在与问题相同的 GitHub link 上找到我更新的 ResizingArrayStack 代码。

      template<typename T> class ResizingArrayStack {
      public:
      
          //----- Begin reversed iteration section -----//
          // Please see the example here, (http://en.cppreference.com/w/cpp/iterator/iterator).
          // Member typedefs inherit from std::iterator.
          class stackIterator: public std::iterator<
                              std::input_iterator_tag,   // iterator_category
                              T,                         // value_type
                              T,                         // difference_type
                              const T*,                  // pointer
                              T                          // reference
                              >{
              int index = 0;
              T* it_ptr = nullptr;
          public:
              // Prefix ++, equal, unequal, and dereference operators are the minimum required for range based for-loops.
              stackIterator(int _index = 0, T* _it_ptr = nullptr) { index = _index; it_ptr = _it_ptr; }
              // Here is where we reverse the sequence.
              stackIterator& operator++() { --index; return *this; }
              bool operator==(stackIterator other) { return index == other.index; }
              bool operator!=(stackIterator other) { return !( *this == other ); }
              T operator*() { return it_ptr[index-1]; }
          };
      
          stackIterator begin() { return stackIterator(N, array_ptr); }
          stackIterator end() {
              N = 0;  // 'Empty' the array.
              max_size = 1;  // Don't waste time calling resize() now. 
              return stackIterator(0, array_ptr);
          }
          //----- End reversed iteration section -----//
      
      private:
          // Allocate space for a traditional array on the heap.
          T* array_ptr = new T[1];
          // Keep track of the space allocated for the array, max_size * sizeof(T).
          int max_size = 1;
          // Keep track of the current number of items on the stack.
          int N = 0;
      

      默认情况下,基于范围的 for 循环以反向(或 LIFO)顺序迭代的调用代码。

      // It's nice performance-wise to iterate in reverse without calling pop() or triggering a resize.
      for ( auto i : lifo_stack) {
          cout << "Current loop iteration has i = " << i << endl;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-12-21
        • 2021-04-08
        • 2016-10-31
        • 2021-08-25
        • 1970-01-01
        相关资源
        最近更新 更多