Skip to content

Commit 4d18c11

Browse files
committed
Update iterator design pattern
1 parent f8ae020 commit 4d18c11

File tree

2 files changed

+227
-40
lines changed

2 files changed

+227
-40
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
- [x] Chapter09:Decorator. 涉及动态装饰器、静态装饰器 和 函数装饰器。
2525
- [x] Chapter10: Facade. 外观模式, 缓冲-视窗-控制台。
2626
- [x] Chapter11: Flyweight. 享元模式。Boost库中Flyweight的实现,以及Bimap
27-
- [ ] Chapter12: Proxy. 翻译了智能指针、属性代理、虚代理。通信代理。
27+
- [ ] Chapter12: Proxy. 翻译了智能指针、属性代理、虚代理。通信代理。
2828
- [ ] Chapter13: Chain of Responsibility. 指针链;代理链涉及中介模式和观察者模式。
2929
- [x] Chapter15: Interpreter.涉及编译原理里面的词法分析,语法分析,`Boost.spirit`的使用。后面会补充LeetCode上实现计算器的几道题目和正则表达式的题目,也许会增加`Lex/Yacc`工具的使用介绍,以及tinySQL解释器实现的简单解释。
30-
- [ ] Chapter16: Iterator.
30+
- [x] Chapter16: Iterator. STL库中的迭代器,涉及二叉树的迭代器,使用协程来简化迭代过程。
3131
- [ ] Chapter17: Mediator.
3232
- [x] Chapter18: Memento.
3333
- [x] Chapter19: Nulll Object. 涉及到对代理模式和pimpl编程技法的运用,以及std::optional

docs/chapter-16-iterator.md

Lines changed: 225 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
### 迭代器
22

3-
在开始处理复杂的数据结构时,都会遇到*遍历(traversal)*的问题。这可以用不同的方法来处理,但最常见的遍历方法,比如,vector,是使用一种称为*迭代器(iterator)*的东西。
3+
在开始处理复杂的数据结构时,都会遇到 *遍历(traversal)* 的问题。这可以用不同的方法来处理,但最常见的遍历方法,比如,`vector`,是使用一种称为 *迭代器(iterator)* 的东西。
44

55
简单地说,迭代器是一个对象,它可以指向集合中的一个元素,也知道如何移动到集合中的下一个元素。因此,只需要实现`++`操作符和`!=`操作符(这样就可以比较两个迭代器并检查它们是否指向相同的东西)。
66

7-
c++标准库大量使用迭代器,因此我们将讨论它们的使用方式,然后我们将看看如何制作我们自己的迭代器以及迭代器的限制是什么
7+
c++标准库大量使用迭代器,因此我们将讨论它们的使用方式,然后我们将看看如何制作我们自己的迭代器以及迭代器的局限
88

99
#### 标准库中的迭代器
1010

@@ -14,43 +14,43 @@ c++标准库大量使用迭代器,因此我们将讨论它们的使用方式
1414
vector<string> names {"john","jane", "jill", "jack"}
1515
```
1616
17-
如果希望获得`names`集合中的第一个名字,则需要调用一个名为`begin()`的函数。这个函数不会按值或引用给你名字;相反,它给您一个迭代器:
17+
如果想要获得`names`中的第一个名字,可以调用`begin()`函数。这个函数不会返回第一个名字的值或引用给你; 相反,它会返回一个迭代器给你:
1818
1919
```c++
2020
vector<string>::iterator it = names.begin();
2121
```
2222

23-
函数begin()既是vector的成员函数,也是全局函数。全局函数对于数组(c风格数组,而不是std::array)特别有用,因为它们不能包含成员函数
23+
`begin()`既是`vector`的成员函数,也是全局函数。全局的`begin()`对于不能包含成员函数`c`语言风格的数组(而不是`std::array`))特别有用。
2424

25-
因此`begin()`返回一个可以视为指针的迭代器:对于`vector`,它具有类似的机制。例如,可以对迭代器进行解引用以打印实际的名称:
25+
你可以把`begin()`返回一个的迭代器看作指针: 对于`vector`来说,功能上是相似的。例如,可以对迭代器进行提领操作(*dereference*)来打印实际的名称:
2626

2727
```c++
2828
cout << "first name is " << *it << "\n";
2929
// first name is john
3030
```
3131

32-
给定的迭代器知道如何前进,即移动到下一个元素。重要的是要意识到`++`指的是向前移动的概念,也就是说,对于指针向前移动(即递增内存地址),它与++是不同的
32+
迭代器`it`是可以 *前进(advance)*,即移动到下一个元素。迭代器上的自增操作`++`强调的是向前移动的概念,也就是说,和指针向前移动的`++`操作(即递增内存地址)是不相同的
3333

3434
```c++
3535
++it; // now points to jane
3636
```
3737

38-
也可以使用迭代器(与指针一样)修改它所指向的元素:
38+
也可以使用迭代器(像指针一样)修改它所指向的元素:
3939

4040
```c++
4141
it->append(" goodall"s);
4242
cout << "full name is " << *it << "\n";
4343
// full name is jane goodall
4444
```
4545
46-
现在,与`begin()`对应的当然是`end()`,但它不是指向最后一个元素,而是指向最后一个元素之后的元素。这是一个拙劣的例证
46+
`begin()`函数对应的当然是`end()`函数,但它不是指向最后一个元素,而是指向最后一个元素之后的元素
4747
4848
```c++
4949
1 2 3 4
5050
begin() ^ ^ end()
5151
```
5252

53-
可以使用`end()`作为终止条件。例如,让我们使用`it`迭代器变量打印其余的名称
53+
可以使用`end()`作为终止条件。例如,让我们使用`it`来打印列表中其余的名称:
5454

5555
```c++
5656
while (++it != names.end())
@@ -61,7 +61,7 @@ while (++it != names.end())
6161
// another name: jack
6262
```
6363

64-
除了`begin()``end()`之外,还有`rbegin()``rend()`它们允许我们在集合中向后移动。在本例中,正如您可能已经猜到的`rbegin()`指向最后一个元素,而`rend()`指向第一个之前的一个元素:
64+
除了`begin()``end()`之外,还有`rbegin()``rend()`它们允许我们在集合中反向移动。在本例中,你可能已经猜到`rbegin()`指向最后一个元素,而`rend()`指向第一个之前的一个元素:
6565

6666
```c++
6767
for (auto ri = rbegin(names); ri != rend(names); ++ri)
@@ -74,27 +74,29 @@ cout << endl;
7474
// jack, jill, jane goodall, john
7575
```
7676

77-
前面有两件事值得指出。首先,即使向后遍历`vector`,我们仍然在迭代器上使用`++`操作符。其次,我们可以做算术:同样,当我写入`ri + 1`时,`this`指向`ri`前面的元素,而不是后面的元素
77+
上面的代码有两点需要注意。首先,即使向后遍历`vector`,我们仍然在迭代器上使用`++`操作符。其次,我们可以对`it`做算术操作: `ri + 1`指向的是`ri`前一个元素,而不是后一个元素
7878

79-
我们也可以有不允许修改对象的常量迭代器:它们通过`cbegin()/cend()`返回,当然,也有相反的类型`crbegin()/crend()`
79+
`STL`中也提供不允许修改对象的常量迭代器:它们通过`cbegin()/cend()`返回,与之对应的是`crbegin()/crend()`
8080

8181
```c++
8282
vector<string>::const_reverse_iterator jack = crbegin(names);
8383
// won't work
8484
*jack += "reacher";
8585
```
8686

87-
最后,值得一提的是现代c++结构,它是一个基于范围的`for`循环,用于从容器的`begin()`一直迭代到`end()`
87+
最后,值得一提的是, 现代c++里面*基于范围的`for`循环(range based for loop)*,从容器的`begin()`一直迭代到`end()`
8888

8989
```c++
9090
for (auto& name : names)
9191
cout << "name = " << name << "\n";
9292
```
9393

94-
注意迭代器在这里是自动解引用的:变量名是一个引用,但也可以按值进行迭代。
94+
注意迭代器在这里是自动提领的: 变量名`name`是一个引用,但也可以按值进行迭代。
9595

9696
### 遍历二叉树
9797

98+
让我们回顾一下数据结构里面遍历二叉树的练习。首先,我们定义树中的的节点:
99+
98100
```c++
99101
template<typename T>
100102
struct Node
@@ -104,51 +106,236 @@ struct Node
104106
Node<T>* right;
105107
Node<T>* parent;
106108
BinaryTree<T>* tree;
107-
explict Node(T value) :
108-
value(value),
109-
left(nullptr),
110-
right(nullptr),
111-
parent(nullptr),
112-
tree(nullptr)
113-
{
114-
115-
};
116-
Node(T value, Node<T>* left, Node<T>* right):
117-
value(value),
118-
left(left),
119-
right(right),
120-
parent(nullptr),
121-
tree(nullptr)
109+
};
110+
```
111+
每个节点都有指向其左孩子结点、右孩子结点,父结点(如果有的话)以及整个树的指针。可以单独构造一个叶节点,也可以使用其子节点来构造内部结点。
112+
113+
```c++
114+
explicit Node(const T& value)
115+
: value(value)
116+
, left(nullptr)
117+
, right(nullptr)
118+
, parent(nullptr)
119+
, tree(nullptr)
120+
{ }
121+
122+
Node(const T& value, Node<T>* left, Node<T>* right)
123+
: value(value)
124+
, left(left)
125+
, right(right)
126+
, parent(nullptr)
127+
, tree(nullptr)
122128
{
123129
this->left->tree = this->right->tree = tree;
124130
this->left->parent = this->right->parent = this;
125131
}
126132
};
127133
```
128-
最后,我们引入一个实用程序成员函数来设置树指针。这是在所有节点的子节点上递归完成的:
134+
135+
最后,我们引入一个通用的成员函数来设置树指针。这是通过在所有节点的子节点上递归完成的:
136+
137+
```c++
138+
void set_tree(BinaryTree<T>* t)
139+
{
140+
tree = t;
141+
if(left)
142+
left->set_tree(t);
143+
if(right)
144+
right->set_tree(t);
145+
}
146+
```
147+
148+
有了这些,我们现在可以定义一个称为`BinaryTree`的结构,它正是这个结构允许迭代:
149+
150+
```c++
151+
template <typename T>
152+
struct BinaryTree
153+
{
154+
Node<T>* root = nullptr;
155+
explicit BinaryTree(Node<T>* const root)
156+
: root{ root }
157+
{
158+
root->set_tree(this);
159+
}
160+
};
161+
```
162+
163+
现在我们可以为树定义一个迭代器。迭代二叉树有三种常见的方法,我们首先要实现的是前序遍历`preorder`:
164+
165+
- 一旦遇到该元素,就返回该元素。
166+
- 递归地遍历左子树
167+
- 递归地遍历右子树
168+
169+
让我们从一个构造函数开始:
170+
171+
```c++
172+
template <typename U>
173+
struct PreOrderIterator
174+
{
175+
Node<U>* current;
176+
explicit PreOrderIterator(Node<U>* current)
177+
: current(current)
178+
{
179+
180+
}
181+
// 其他成员
182+
};
183+
```
184+
185+
需要定义`operator != `来与其他迭代器进行比较。因为迭代器的相当于指针,所以这很简单:
186+
187+
```c++
188+
bool operator!=(const PreOrderIterator<U>& other)
189+
{
190+
return this->current != other.current;
191+
}
192+
```
193+
194+
我们需要定义`*`操作符来实现提领:
195+
196+
```c++
197+
Node<U>& operator*() { return *current; }
198+
```
199+
200+
201+
现在,最后一个问题是如何从我们的二叉树中把迭代器给暴露出来。如果将前序遍历其定义为树的默认迭代器,则可以如下所示补充成员函数:
202+
203+
```c++
204+
typedef PreOrderIterator<T> iterator;
205+
206+
iterator begin()
207+
{
208+
Node<T>* n = root;
209+
if(n)
210+
while(n->left)
211+
n = n->left;
212+
return iterator { n };
213+
}
214+
215+
iterator end()
216+
{
217+
return iterator {nullptr};
218+
}
219+
```
220+
221+
现在,困难的部分来了:遍历树。这里的挑战是我们使用递归算法,遍历发生在`++`操作符中,所以我们最终实现如下所示:
222+
223+
```c++
224+
PreOrderIterator<U>& operator++()
225+
{
226+
if(current->right)
227+
{
228+
current = current->right;
229+
while(current->left)
230+
current = current->left;
231+
}
232+
else
233+
{
234+
Node<T>* p = current->parent;
235+
while(p && current == p->right)
236+
{
237+
current = p;
238+
p = p->parent;
239+
}
240+
current = p;
241+
}
242+
return *this;
243+
}
244+
```
245+
246+
这太乱了!而且,它看起来一点也不像树遍历的经典实现,因为我们没有递归。
247+
248+
值得注意的是,`begin()`迭代器并不是从整个树的根开始; 相反,它从最左边可用的节点开始。
249+
250+
现在所有的部分都准备好了,下面是我们如何执行遍历:
251+
252+
```c++
253+
BinaryTree<string> family{
254+
new Node<string>{
255+
"me",
256+
new Node<string>{
257+
"mother",
258+
new Node<string>{"mother's mother"},
259+
new Node<string>{"mother's father"}},
260+
new Node<string>{"father"}
261+
}
262+
};
263+
264+
for (auto it = family.begin(); it != family.end(); ++it)
265+
{
266+
cout << (*it).value << "\n";
267+
}
268+
```
269+
270+
您也可以将这种遍历形式暴露为一个单独的对象,即:
271+
129272
```c++
273+
class pre_order_traversal
274+
{
275+
BinaryTree<T>& tree;
276+
public:
277+
pre_order_traversal(BinaryTree<T>& tree) : tree{ tree } {}
278+
iterator begin() { return tree.begin(); }
279+
iterator end() { return tree.end(); }
280+
} pre_order;
281+
```
130282
283+
可以这样来遍历:
284+
285+
```c++
286+
for (const auto& it: family.pre_order)
287+
{
288+
cout << it.value << "\n";
289+
}
131290
```
132291

292+
类似地,可以定义`in_order``post_order`遍历算法来暴露合适的迭代器。
133293

134294
### 迭代与协程
135295

136-
我们有一个严重的问题:在遍历代码中,operator++是一团难以读懂的混乱,它与你在Wikipedia上读到的关于树遍历的任何内容都不匹配。它能起作用,但它之所以能起作用,只是因为我们将迭代器预初始化为从最左边的节点开始,而不是从根节点开始,也可以说,这也是有问题和令人困惑的
296+
我们遇到一个严重的问题:在遍历代码中,`operator++`难以读懂,它与你在`Wikipedia`上读到的关于树遍历的任何内容都不匹配。它能起作用,但它之所以能起作用,只是因为我们将迭代器初始化为最左边的节点,而不是从根节点开始,这样做是有问题和令人困惑的
137297

138-
因此,递归是不可能的。现在,如果我们有一种既能吃蛋糕又能吃蛋糕的机制:可以执行正确递归的可恢复函数?这就是`协程(coroutines)`的作用。
139298

299+
为什么我们会有这个问题?因为`++`操作符是不可恢复的:它不能在两次调用之间保持其堆栈,因此,不能实现递归。现在,我们是否有一种两全其美的方法:可执行正确递归的可恢复函数? 我们可以用`协程(coroutines)`来实现。
140300

301+
利用协程,我们可以像这样实现后序遍历:
141302

142-
### 总结
303+
```c++
304+
generator<<Node<T>*> post_order_impl(Node<T>* node)
305+
{
306+
if(node)
307+
{
308+
for(auto x : post_order_impl(node->left))
309+
co_yield x;
310+
for(auto y : post_order_impl(node->right))
311+
co_yield y;
312+
co_yield node;
313+
}
314+
}
315+
generator<Node<T>*> post_order()
316+
{
317+
return post_order_impl(root);
318+
}
319+
```
320+
321+
这不是很棒吗?算法终于又可读了!而且,看不到`begin()/end()`: 我们只是返回一个 *生成器(generator)* ,逐步返回`co_yield`提供生成的值。在生成每个值之后,我们可以挂起当前操作并执行其他操作(例如,打印值),然后在不丢失上下文的情况下恢复迭代。这使的递归成为可能并允许我们写出如下的代码:
322+
323+
```c++
324+
for(auto it : family.post_order())
325+
{
326+
cout << it->value << endl;
327+
}
328+
```
329+
330+
协程是c++的未来,它解决了许多传统迭代器丑陋或不合适的问题。
143331

144-
迭代器设计模式在c++中无处不在,有显式的也有隐式的(例如基于范围的)形式。不同类型的迭代器可以用于迭代不同的对象:例如,反向迭代器可以应用于vector对象,但不能应用于单链表。
145332

146-
实现自己的迭代器就像提供++和!=操作符一样简单。大多数迭代器都是简单的指针`façades`,在它们被丢弃之前,用于遍历集合一次。
333+
### 总结
147334

148-
协程修复了迭代器中出现的一些问题:它们允许在调用之间保持状态,这是其他语言(如c#)很久以前就实现了的。因此,协程允许我们编写递归算法
335+
迭代器设计模式在c++中无处不在,有显式的也有隐式的(例如基于范围的)形式。不同类型的迭代器可以用于迭代不同的对象:例如,反向迭代器可以应用于`vector`,但不能应用于单链表
149336

150-
通过协程,我们可以实现后序树遍历,如下所示:
337+
实现自己的迭代器就像提供`++``!=`操作符一样简单。大多数迭代器都是简单的指针的外观(`façades`),在它们被丢弃之前,用于遍历集合一次。
151338

339+
协程修复了迭代器中出现的一些问题:它们允许在调用之间保持状态,这是其他语言(如`c#`)很久以前就实现了的。因此,协程允许我们编写递归算法。
152340

153341

154-
协程是c++的未来,它解决了许多传统迭代器丑陋或不合适的问题。

0 commit comments

Comments
 (0)