Skip to content

Commit 7776a59

Browse files
committed
book: translation of ch04
Update #12
1 parent bf6b092 commit 7776a59

9 files changed

+412
-69
lines changed

book/en-us/04-containers.md

+295-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,302 @@ order: 4
66

77
# Chapter 04 Containers
88

9-
[Table of Content](./toc.md) | [Previous Chapter](./03-runtime.md) | [Next Chapter: Smart Pointers and Memory Management](./05-pointers.md)
9+
[TOC]
10+
11+
## 4.1 Linear Container
12+
13+
### `std::array`
14+
15+
When you see this container, you will definitely have this problem:
16+
17+
1. Why introduce `std::array` instead of `std::vector` directly?
18+
2. Already have a traditional array, why use `std::array`?
19+
20+
First answer the first question. Unlike `std::vector`, the size of the `std::array` object is fixed. If the container size is fixed, then the `std::array` container can be used first.
21+
In addition, since `std::vector` is automatically expanded, when a large amount of data is stored, and the container is deleted,
22+
The container does not automatically return the corresponding memory of the deleted element. In this case, you need to manually run `shrink_to_fit()` to release this part of the memory.
23+
24+
```cpp
25+
std::vector<int> v;
26+
std::cout << "size:" << v.size() << std::endl; // output 0
27+
std::cout << "capacity:" << v.capacity() << std::endl; // output 0
28+
29+
// As you can see, the storage of std::vector is automatically managed and
30+
// automatically expanded as needed.
31+
// But if there is not enough space, you need to redistribute more memory,
32+
// and reallocating memory is usually a performance-intensive operation.
33+
v.push_back(1);
34+
v.push_back(2);
35+
v.push_back(3);
36+
std::cout << "size:" << v.size() << std::endl; // output 3
37+
std::cout << "capacity:" << v.capacity() << std::endl; // output 4
38+
39+
// The auto-expansion logic here is very similar to Golang's slice.
40+
v.push_back(4);
41+
v.push_back(5);
42+
std::cout << "size:" << v.size() << std::endl; // output 5
43+
std::cout << "capacity:" << v.capacity() << std::endl; // output 8
44+
45+
// As can be seen below, although the container empties the element,
46+
// the memory of the emptied element is not returned.
47+
v.clear();
48+
std::cout << "size:" << v.size() << std::endl; // output 0
49+
std::cout << "capacity:" << v.capacity() << std::endl; // output 8
50+
51+
// Additional memory can be returned to the system via the shrink_to_fit() call
52+
v.shrink_to_fit();
53+
std::cout << "size:" << v.size() << std::endl; // output 0
54+
std::cout << "capacity:" << v.capacity() << std::endl; // output 0
55+
```
56+
57+
The second problem is much simpler. Using `std::array` can make the code more "modern" and encapsulate some manipulation functions, such as getting the array size and checking if it is not empty, and also using the standard friendly. Container algorithms in the library, such as `std::sort`.
58+
59+
Using `std::array` is as simple as specifying its type and size:
60+
61+
```cpp
62+
std::array<int, 4> arr = {1, 2, 3, 4};
63+
64+
arr.empty(); // check if container is empty
65+
arr.size(); // return the size of the container
66+
67+
// iterator support
68+
for (auto &i : arr)
69+
{
70+
// ...
71+
}
72+
73+
// use lambda expression for sort
74+
std::sort(arr.begin(), arr.end(), [](int a, int b) {
75+
return b < a;
76+
});
77+
78+
// array size must be constexpr
79+
constexpr int len = 4;
80+
std::array<int, len> arr = {1, 2, 3, 4};
81+
82+
// illegal, different than C-style array, std::array will not deduce to T*
83+
// int *arr_p = arr;
84+
```
85+
86+
When we started using `std::array`, it was inevitable that we would encounter a C-style compatible interface. There are three ways to do this:
87+
88+
```cpp
89+
void foo(int *p, int len) {
90+
return;
91+
}
92+
93+
std::array<int, 4> arr = {1,2,3,4};
94+
95+
// C-stype parameter passing
96+
// foo(arr, arr.size()); // illegal, cannot convert implicitly
97+
foo(&arr[0], arr.size());
98+
foo(arr.data(), arr.size());
99+
100+
// use `std::sort`
101+
std::sort(arr.begin(), arr.end());
102+
```
103+
104+
### `std::forward_list`
105+
106+
`std::forward_list` is a list container, and the usage is basically similar to `std::list`, so we don't spend a lot of time introducing it.
107+
108+
Need to know is that, unlike the implementation of the doubly linked list of `std::list`, `std::forward_list` is implemented using a singly linked list.
109+
Provides element insertion of `O(1)` complexity, does not support fast random access (this is also a feature of linked lists),
110+
It is also the only container in the standard library container that does not provide the `size()` method. Has a higher space utilization than `std::list` when bidirectional iteration is not required.
111+
112+
## 4.2 Unordered Container
113+
114+
We are already familiar with the ordered container `std::map`/`std::set` in traditional C++. These elements are internally implemented by red-black trees.
115+
The average complexity of inserts and searches is `O(log(size))`. When inserting an element, the element size is compared according to the `<` operator and the element is determined to be the same.
116+
And select the appropriate location to insert into the container. When traversing the elements in this container, the output will be traversed one by one in the order of the `<` operator.
117+
118+
The elements in the unordered container are not sorted, and the internals are implemented by the Hash table. The average complexity of inserting and searching for elements is `O(constant)`,
119+
Significant performance gains can be achieved without concern for the order of the elements inside the container.
120+
121+
C++11 introduces two sets of unordered containers: `std::unordered_map`/`std::unordered_multimap` and
122+
`std::unordered_set`/`std::unordered_multiset`.
123+
124+
Their usage is basically similar to the original `std::map`/`std::multimap`/`std::set`/`set::multiset`
125+
Since these containers are already familiar to us, we will not compare them one by one. Let's compare `std::map` and `std::unordered_map` directly:
126+
127+
```cpp
128+
#include <iostream>
129+
#include <string>
130+
#include <unordered_map>
131+
#include <map>
132+
133+
int main() {
134+
// initialized in same order
135+
std::unordered_map<int, std::string> u = {
136+
{1, "1"},
137+
{3, "3"},
138+
{2, "2"}
139+
};
140+
std::map<int, std::string> v = {
141+
{1, "1"},
142+
{3, "3"},
143+
{2, "2"}
144+
};
145+
146+
// iterates in the same way
147+
std::cout << "std::unordered_map" << std::endl;
148+
for( const auto & n : u)
149+
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
150+
151+
std::cout << std::endl;
152+
std::cout << "std::map" << std::endl;
153+
for( const auto & n : v)
154+
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
155+
}
156+
```
10157

11-
## Further Readings
158+
The final output is:
159+
160+
```txt
161+
std::unordered_map
162+
Key:[2] Value:[2]
163+
Key:[3] Value:[3]
164+
Key:[1] Value:[1]
165+
166+
std::map
167+
Key:[1] Value:[1]
168+
Key:[2] Value:[2]
169+
Key:[3] Value:[3]
170+
```
171+
172+
## 4.3 Tuples
173+
174+
Programmers who have known Python should be aware of the concept of tuples. Looking at the containers in traditional C++, except for `std::pair`
175+
There seems to be no ready-made structure to store different types of data (usually we will define the structure ourselves).
176+
But the flaw of `std::pair` is obvious, only two elements can be saved.
177+
178+
### Basic Operations
179+
180+
There are three core functions for the use of tuples:
181+
182+
1. `std::make_tuple`: construct tuple
183+
2. `std::get`: Get the value of a position in the tuple
184+
3. `std::tie`: tuple unpacking
185+
186+
```cpp
187+
#include <tuple>
188+
#include <iostream>
189+
190+
auto get_student(int id) {
191+
if (id == 0)
192+
return std::make_tuple(3.8, 'A', "John");
193+
if (id == 1)
194+
return std::make_tuple(2.9, 'C', "Jack");
195+
if (id == 2)
196+
return std::make_tuple(1.7, 'D', "Ive");
197+
198+
// it is not allowed to return 0 directly
199+
// return type is std::tuple<double, char, std::string>
200+
return std::make_tuple(0.0, 'D', "null");
201+
}
202+
203+
int main() {
204+
auto student = get_student(0);
205+
std::cout << "ID: 0, "
206+
<< "GPA: " << std::get<0>(student) << ", "
207+
<< "Grade: " << std::get<1>(student) << ", "
208+
<< "Name: " << std::get<2>(student) << '\n';
209+
210+
double gpa;
211+
char grade;
212+
std::string name;
213+
214+
// unpack tuples
215+
std::tie(gpa, grade, name) = get_student(1);
216+
std::cout << "ID: 1, "
217+
<< "GPA: " << gpa << ", "
218+
<< "Grade: " << grade << ", "
219+
<< "Name: " << name << '\n';
220+
}
221+
```
222+
223+
`std::get` In addition to using constants to get tuple objects, C++14 adds usage types to get objects in tuples:
224+
225+
```cpp
226+
std::tuple<std::string, double, double, int> t("123", 4.5, 6.7, 8);
227+
std::cout << std::get<std::string>(t) << std::endl;
228+
std::cout << std::get<double>(t) << std::endl; // illegal, runtime error
229+
std::cout << std::get<3>(t) << std::endl;
230+
```
231+
232+
### Runtime Indexing
233+
234+
If you think about it, you might find the problem with the above code. `std::get<>` depends on a compile-time constant, so the following is not legal:
235+
236+
```cpp
237+
int index = 1;
238+
std::get<index>(t);
239+
```
240+
241+
So what do you do? The answer is to use `std::variant<>` (introduced by C++ 17) to provide type template parameters for `variant<>`
242+
You can have a `variant<>` to accommodate several types of variables provided (in other languages, such as Python/JavaScript, etc., as dynamic types):
243+
244+
```cpp
245+
#include <variant>
246+
template <size_t n, typename... T>
247+
constexpr std::variant<T...> _tuple_index(const std::tuple<T...>& tpl, size_t i) {
248+
if constexpr (n >= sizeof...(T))
249+
throw std::out_of_range("越界.");
250+
if (i == n)
251+
return std::variant<T...>{ std::in_place_index<n>, std::get<n>(tpl) };
252+
return _tuple_index<(n < sizeof...(T)-1 ? n+1 : 0)>(tpl, i);
253+
}
254+
template <typename... T>
255+
constexpr std::variant<T...> tuple_index(const std::tuple<T...>& tpl, size_t i) {
256+
return _tuple_index<0>(tpl, i);
257+
}
258+
template <typename T0, typename ... Ts>
259+
std::ostream & operator<< (std::ostream & s, std::variant<T0, Ts...> const & v) {
260+
std::visit([&](auto && x){ s << x;}, v);
261+
return s;
262+
}
263+
```
264+
265+
So we can:
266+
267+
```cpp
268+
int i = 1;
269+
std::cout << tuple_index(t, i) << std::endl;
270+
```
271+
272+
### Merge and Iteration
273+
274+
Another common requirement is to merge two tuples, which can be done with `std::tuple_cat`:
275+
276+
```cpp
277+
auto new_tuple = std::tuple_cat(get_student(1), std::move(t));
278+
```
279+
280+
You can immediately see how quickly you can traverse a tuple? But we just introduced how to index a `tuple` by a very number at runtime, then the traversal becomes simpler.
281+
First we need to know the length of a tuple, which can:
282+
283+
```cpp
284+
template <typename T>
285+
auto tuple_len(T &tpl) {
286+
return std::tuple_size<T>::value;
287+
}
288+
```
289+
290+
This will iterate over the tuple:
291+
292+
```cpp
293+
for(int i = 0; i != tuple_len(new_tuple); ++i)
294+
// runtime indexing
295+
std::cout << tuple_index(i, new_tuple) << std::endl;
296+
```
297+
298+
## Conclusion
299+
300+
This chapter briefly introduces the new containers in modern C++. Their usage is similar to that of the existing containers in C++. It is relatively simple, and you can choose the containers you need to use according to the actual scene, so as to get better performance.
301+
302+
Although `std::tuple` is effective, the standard library provides limited functionality and there is no way to meet the requirements of runtime indexing and iteration. Fortunately, we have other methods that we can implement on our own.
303+
304+
[Table of Content](./toc.md) | [Previous Chapter](./03-runtime.md) | [Next Chapter: Smart Pointers and Memory Management](./05-pointers.md)
12305

13306
## Licenses
14307

book/en-us/toc.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@
5656
+ Move semantics
5757
+ Perfect forwarding
5858
- [**Chapter 04 Containers**](./04-containers.md)
59-
+ 4.1 `std::array` and `std::forward_list`
59+
+ 4.1 Linear containers
60+
+ `std::array`
61+
+ `std::forward_list`
6062
+ 4.2 Unordered containers
61-
+ `std::unordered_set`
62-
+ `std::unordered_map`
63+
+ `std::unordered_set`
64+
+ `std::unordered_map`
6365
+ 4.3 Tuples `std::tuple`
6466
+ basic operation
65-
+ runtime indexing
67+
+ runtime indexing `std::variant`
6668
+ merge and iteration
6769
- [**Chapter 05 Smart Pointers and Memory Management**](./05-pointers.md)
6870
+ 5.1 RAII and reference counting

book/zh-cn/04-containers.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ order: 4
88

99
[TOC]
1010

11-
## 4.1 `std::array``std::forward_list`
11+
## 4.1 线性容器
1212

1313
### `std::array`
1414

@@ -166,7 +166,7 @@ Key:[2] Value:[2]
166166
Key:[3] Value:[3]
167167
```
168168

169-
## 4.3 元组 `std::tuple`
169+
## 4.3 元组
170170

171171
了解过 Python 的程序员应该知道元组的概念,纵观传统 C++ 中的容器,除了 `std::pair` 外,
172172
似乎没有现成的结构能够用来存放不同类型的数据(通常我们会自己定义结构)。
@@ -297,7 +297,7 @@ for(int i = 0; i != tuple_len(new_tuple); ++i)
297297

298298
## 总结
299299

300-
本节简单介绍了 C++11/14 中新增的容器,它们的用法和传统 C++ 中已有的容器类似,相对简单,可以根据实际场景丰富的选择需要使用的容器,从而获得更好的性能。
300+
本章简单介绍了现代 C++ 中新增的容器,它们的用法和传统 C++ 中已有的容器类似,相对简单,可以根据实际场景丰富的选择需要使用的容器,从而获得更好的性能。
301301

302302
`std::tuple` 虽然有效,但是标准库提供的功能有限,没办法满足运行期索引和迭代的需求,好在我们还有其他的方法可以自行实现。
303303

book/zh-cn/toc.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@
5656
+ 移动语义
5757
+ 完美转发
5858
- [**第 4 章 标准库: 容器**](./04-containers.md)
59-
+ 4.1 `std::array``std::forward_list`
59+
+ 4.1 线性容器
60+
+ `std::array`
61+
+ `std::forward_list`
6062
+ 4.2 无序容器
6163
+ `std::unordered_set`
6264
+ `std::unordered_map`
6365
+ 4.3 元组 `std::tuple`
6466
+ 基本操作
65-
+ 运行期索引
67+
+ 运行期索引 `std::variant`
6668
+ 合并与迭代
6769
- [**第 5 章 标准库: 指针**](./05-pointers.md)
6870
+ 5.1 RAII 与引用计数

0 commit comments

Comments
 (0)