在自定义容器类上反转基于范围的 for 循环

Reversing range-based for loop on a custom container class

本文关键字:范围 循环 for 于范围 容器类 自定义      更新时间:2023-10-16

我试图通过移植Sedgewick和Wayne的Algorithms,第4版中的主要示例来提高我的C++技能。我根据他们的 Java 示例编写了一个通用堆栈实现。

我的堆栈工作正常,但我想提高性能,并在尝试编写反向迭代器时陷入困境。

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反向循环(相对于堆栈的后进先出)?

如果有严重的错误或看起来过时的方面,请随时评论我的代码风格。我想与良好的"现代"C++保持一致。

如果要将 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 << ',';    
}
}

现场示例

请注意,这使用 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 << ',';    
}

现场示例(从 gcc 5.1 或 clang 3.5 开始工作)

或者,您可以使用Boost.Range 库并简单地执行(将适用于任何 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 << ',';    
}
}

现场示例

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

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

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); }
// ...  //
};

一旦你有了功能(常规)迭代器, 使用标准库实现反向迭代器 帮助程序类模板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()}; }

查看完整的代码lifo_stack.pop()会使迭代器失效,因此它不能在ranged 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 的规则。

此解决方案不会引入不必要的副本,也不会像某些注释所建议的那样表现出不正确的转发。解释如下。

您可以使用一些包装器,该包装器具有实际上具有开始和结束函数 返回反向迭代器。

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

由于 foward 不是一个容易的话题,并且存在误解,我将进一步解释一些细节。我将以std::vector<int>为例,说明"要反转"的类型T

1. 函数模板reverse

1.1 传递左值std::vector<int>

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<int>&,因此不涉及副本。
  • 我们将左值
  • 作为左值转发给revert_wrapper的构造函数

1.2 传递右值std::vector<int>

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<int>,从而复制向量,防止右值在任何基于范围的循环运行之前超出范围
  • 右值std::vector<int>&&将转发给revert_wrapper的构造函数

 

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

2.1reverse在左值的情况下创建的revert_wrapperstd::vector<int>&

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.2reverse在右值的情况下创建的revert_wrapperstd::vector<int>&&

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<T&&>(i),但我们不是从左值(std::)mov(e),我们正在转发:

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

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

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

请在此处查看TemplateRex的出色答案。我能够在没有包装类的情况下解决问题,所以我会尝试回答我自己的问题。

这是我在 http://en.cppreference.com 上找到的关于实现迭代器的最有用的示例,您可以在发现问题的同一GitHub链接中找到我更新的RessizeArrayStack代码。

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