Skip to content

Commit 3bf1cee

Browse files
committed
deploy: 9d0cee6
1 parent 4207816 commit 3bf1cee

File tree

4 files changed

+129
-51
lines changed

4 files changed

+129
-51
lines changed

index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ <h2 id="_1">前言</h2>
248248
<blockquote>
249249
<p><img src="./img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 本书还在持续更新中……要追番的话,可以在 <a href="https://github.com/parallel101/cppguidebook">GitHub</a> 点一下右上角的 “Watch” 按钮,每当小彭老师提交新 commit,GitHub 会向你发送一峰电子邮件,提醒你小彭老师更新了。</p>
250250
</blockquote>
251-
<p>更新时间:2024年08月03日 16:38:36 (UTC+08:00)</p>
251+
<p>更新时间:2024年08月03日 19:36:10 (UTC+08:00)</p>
252252
<h2 id="_2">格式约定</h2>
253253
<blockquote>
254254
<p><img src="./img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 用这种颜色字体书写的内容是温馨提示</p>

print_page/index.html

+64-25
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ <h2 id="index-_1">前言</h2>
345345
<blockquote>
346346
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 本书还在持续更新中……要追番的话,可以在 <a href="https://github.com/parallel101/cppguidebook">GitHub</a> 点一下右上角的 “Watch” 按钮,每当小彭老师提交新 commit,GitHub 会向你发送一峰电子邮件,提醒你小彭老师更新了。</p>
347347
</blockquote>
348-
<p>更新时间:2024年08月03日 16:38:36 (UTC+08:00)</p>
348+
<p>更新时间:2024年08月03日 19:36:10 (UTC+08:00)</p>
349349
<h2 id="index-_2">格式约定</h2>
350350
<blockquote>
351351
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 用这种颜色字体书写的内容是温馨提示</p>
@@ -5988,6 +5988,7 @@ <h2 id="stl_map-_28">一边遍历一边删除部分元素</h2>
59885988
<p>你拿着1号朋友家的地址,一发 RPG 导弹把他家炸了。然后你现在突然意识到需要2号朋友家的地址,但是1号朋友家已经被你炸了,你傻乎乎进入燃烧的1号朋友家,被火烧死了。</p>
59895989
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ++it) {
59905990
m.erase(it);
5991+
// it 已经失效!
59915992
}
59925993
</code></pre>
59935994
<p>正确的做法是,先进入1号朋友家,安全取出写着2号朋友家地址的字条后,再来一发 RPG 把1号朋友家炸掉。这样才能顺利找到2号朋友家,以此类推继续拆3号……</p>
@@ -6034,7 +6035,6 @@ <h2 id="stl_map-_28">一边遍历一边删除部分元素</h2>
60346035
<hr />
60356036
<!-- PG109 -->
60366037

6037-
<p>::left::</p>
60386038
<p>不奔溃</p>
60396039
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ) {
60406040
auto const &amp;[k, v] = *it;
@@ -6045,7 +6045,6 @@ <h2 id="stl_map-_28">一边遍历一边删除部分元素</h2>
60456045
}
60466046
}
60476047
</code></pre>
6048-
<p>::right::</p>
60496048
<p>奔溃</p>
60506049
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ++it) {
60516050
auto const &amp;[k, v] = *it;
@@ -6128,8 +6127,8 @@ <h3 id="stl_map-c20-erase_if">C++20 更好的写法:erase_if</h3>
61286127
<p>他的参数类型就是刚刚介绍的 <code>value_type</code>,也就是 <code>pair&lt;const K, V&gt;</code>。</p>
61296128
<p>pair 是一个 STL 中常见的模板类型,<code>pair&lt;K, V&gt;</code> 有两个成员变量:</p>
61306129
<ul>
6131-
<li>first:V 类型,表示要插入元素的键</li>
6132-
<li>second:K 类型,表示要插入元素的值</li>
6130+
<li>first:K 类型,表示要插入元素的键</li>
6131+
<li>second:V 类型,表示要插入元素的值</li>
61336132
</ul>
61346133
<p>我称之为&rdquo;键值对&rdquo;。</p>
61356134
<hr />
@@ -6188,15 +6187,13 @@ <h3 id="stl_map-c20-erase_if">C++20 更好的写法:erase_if</h3>
61886187
<li>异:当键 K 已经存在时,insert 不会覆盖,默默离开;而 [] 会覆盖旧的值。</li>
61896188
</ul>
61906189
<p>例子:</p>
6191-
<p>::left::</p>
61926190
<pre><code class="language-cpp">map&lt;string, string&gt; m;
61936191
m.insert({&quot;key&quot;, &quot;old&quot;});
61946192
m.insert({&quot;key&quot;, &quot;new&quot;}); // 插入失败,默默放弃不出错
61956193
print(m);
61966194
</code></pre>
61976195
<pre><code>{&quot;key&quot;: &quot;old&quot;}
61986196
</code></pre>
6199-
<p>::right::</p>
62006197
<pre><code class="language-cpp">map&lt;string, string&gt; m;
62016198
m[&quot;key&quot;] = &quot;old&quot;;
62026199
m[&quot;key&quot;] = &quot;new&quot;; // 已经存在?我踏马强行覆盖!
@@ -6384,6 +6381,23 @@ <h3 id="stl_map-insert_1">批量 insert 同样遵循不覆盖原则</h3>
63846381
</code></pre>
63856382
<pre><code>{&quot;delay&quot;: 211, &quot;timeout&quot;: 985}
63866383
</code></pre>
6384+
<pre><code class="language-cpp">vector&lt;pair&lt;string, int&gt;&gt; kvs = {
6385+
{&quot;timeout&quot;, 985},
6386+
{&quot;delay&quot;, 211},
6387+
{&quot;delay&quot;, 666},
6388+
{&quot;delay&quot;, 233},
6389+
{&quot;timeout&quot;, 996},
6390+
};
6391+
map&lt;string, int&gt; config = {
6392+
{&quot;timeout&quot;, 404},
6393+
};
6394+
config.insert(kvs.begin(), kvs.end());
6395+
print(config);
6396+
6397+
vector&lt;unique_ptr&lt;int&gt;&gt; v;
6398+
</code></pre>
6399+
<pre><code>{&quot;delay&quot;: 211, &quot;timeout&quot;: 404}
6400+
</code></pre>
63876401
<!-- PG127 -->
63886402

63896403
<h3 id="stl_map-insert-map">批量 insert 实现 map 合并</h3>
@@ -6814,8 +6828,9 @@ <h3 id="stl_map-emplace_1">emplace 的原理和优点</h3>
68146828
<li>不建议在 map 上使用 emplace/emplace_hint,请改用 try_emplace。</li>
68156829
</ul>
68166830
<h2 id="stl_map-try_emplace">try_emplace 更好</h2>
6817-
<p>emplacec 只支持 pair 的就地构造,这有什么用?我们要的是 pair 中值类型的就地构造!这就是 try_emplace 的作用了,他对 key 部分依然是传统的移动,只对 value 部分采用就地构造。</p>
6831+
<p>emplace 只支持 pair 的就地构造,这有什么用?我们要的是 pair 中值类型的就地构造!这就是 try_emplace 的作用了,他对 key 部分依然是传统的移动,只对 value 部分采用就地构造。</p>
68186832
<blockquote>
6833+
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 这是观察到大多是值类型很大,急需就地构造,而键类型没用多少就地构造的需求。例如 <code>map&lt;string, array&lt;int, 1000&gt;&gt;</code></p>
68196834
<p><img src="../img/question.png" height="30px" width="auto" style="margin: 0; border: none"/> 如果想不用 try_emplace,完全基于 emplace 实现针对值 value 的就地构造需要用到 std::piecewise_construct 和 std::forward_as_tuple,非常麻烦。</p>
68206835
</blockquote>
68216836
<p>insert 的托马斯黄金大回旋分奴版:try_emplace(C++17 引入)</p>
@@ -6828,6 +6843,12 @@ <h2 id="stl_map-try_emplace">try_emplace 更好</h2>
68286843
<p>他等价于:</p>
68296844
<pre><code class="language-cpp">m.insert({key, V(arg1, arg2, ...)});
68306845
</code></pre>
6846+
<p>后面的变长参数也可以完全没有:</p>
6847+
<pre><code class="language-cpp">m.try_emplace(key);
6848+
</code></pre>
6849+
<p>他等价于调用 V 的默认构造函数:</p>
6850+
<pre><code class="language-cpp">m.insert({key, V()});
6851+
</code></pre>
68316852
<p>由于 emplace 实在是憨憨,他变长参数列表就地构造的是 pair,然而 pair 的构造函数正常不就是只有两个参数吗,变长没有用。实际有用的往往是我们希望用变长参数列表就地构造值类型 V,对 K 部分并不关系。因此 C++17 引入了 try_emplace,其键部分保持 <code>K const &amp;</code>,值部分采用变长参数列表。</p>
68326853
<p>我的评价是:这个比 emplace 实用多了,如果要与 vector 的 emplace_back 对标,那么 map 与之对应的一定是 try_emplace。同学们如果要分奴的话还是建议用 try_emplace。</p>
68336854
<h3 id="stl_map-try_emplace_1">try_emplace 可以避免移动!</h3>
@@ -6847,9 +6868,9 @@ <h3 id="stl_map-try_emplace_1">try_emplace 可以避免移动!</h3>
68476868
m.try_emplace(&quot;key&quot;, 42); // MyClass(int)
68486869
m.try_emplace(&quot;key&quot;, &quot;hell&quot;, 3.14f); // MyClass(const char *, float)
68496870
// 等价于:
6850-
m.insert({&quot;key&quot;, {}}); // MyClass()
6851-
m.insert({&quot;key&quot;, {42}}); // MyClass(int)
6852-
m.insert({&quot;key&quot;, {&quot;hell&quot;, 3.14f}}); // MyClass(const char *, float)
6871+
m.insert({&quot;key&quot;, MyClass()}); // MyClass()
6872+
m.insert({&quot;key&quot;, MyClass(42)}); // MyClass(int)
6873+
m.insert({&quot;key&quot;, MyClass(&quot;hell&quot;, 3.14f)}); // MyClass(const char *, float)
68536874
</code></pre>
68546875
<p>对于移动开销较大的类型(例如 <code>array&lt;int, 1000&gt;</code>),try_emplace 可以避免移动;对于不支持移动构造函数的值类型,就必须使用 try_emplace 了。</p>
68556876
<!-- PG146 -->
@@ -6858,6 +6879,7 @@ <h3 id="stl_map-try_emplace_2">谈谈 try_emplace 的优缺点</h3>
68586879
<pre><code class="language-cpp">// 以下两种方式效果等价,只有性能不同
68596880
m.try_emplace(key, arg1, arg2, ...); // 开销:1次构造函数
68606881
m.insert({key, V(arg1, arg2, ...)}); // 开销:1次构造函数 + 2次移动函数
6882+
m.insert(make_pair(key, V(arg1, arg2, ...))); // 开销:1次构造函数 + 3次移动函数
68616883
</code></pre>
68626884
<p>但是由于 try_emplace 是用圆括号帮你调用的构造函数,而不是花括号初始化。</p>
68636885
<p>导致你要么无法省略类型,要么你得手动定义类的构造函数:</p>
@@ -7388,14 +7410,27 @@ <h4 id="stl_map-_48">用途举例</h4>
73887410
<p>调用者稍后可以直接销毁这个特殊智能指针:</p>
73897411
<pre><code class="language-cpp">{
73907412
auto node = m.extract(&quot;fuck&quot;);
7391-
print(node.key(), node.value());
7413+
print(node.key(), node.mapped());
73927414
} // node 在此自动销毁
73937415
</code></pre>
73947416
<p>也可以做一些修改后(例如修改键值),稍后重新用 insert(node) 重新把他插入回去:</p>
73957417
<pre><code class="language-cpp">auto node = m.extract(&quot;fuck&quot;);
7396-
nh.key() = &quot;love&quot;;
7418+
node.key() = &quot;love&quot;;
73977419
m.insert(std::move(node));
73987420
</code></pre>
7421+
<blockquote>
7422+
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 过去,通过迭代器来修改键值是不允许的:</p>
7423+
</blockquote>
7424+
<pre><code class="language-cpp">map&lt;string, int&gt; m;
7425+
auto it = m.find(&quot;fuck&quot;);
7426+
assert(it != m.end());
7427+
// *it 是 pair&lt;const string, int&gt;
7428+
it-&gt;first = &quot;love&quot;; // 错误!first 是 const string 类型
7429+
m.insert(*it);
7430+
</code></pre>
7431+
<blockquote>
7432+
<p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 因为直接修改在 map 里面的一个节点的键,会导致排序失效,破坏红黑树的有序。而 extract 取出来的游离态节点,可以修改 <code>.key()</code>,不会影响任何红黑树的顺序,他已经不在树里面了。</p>
7433+
</blockquote>
73997434
<p>或者插入到另一个不同的 map 对象(但键和值类型相同)里:</p>
74007435
<pre><code class="language-cpp">// 从 m1 挪到 m2
74017436
auto node = m1.extract(&quot;fuck&quot;);
@@ -7646,10 +7681,10 @@ <h4 id="stl_map-insert-vs-merge">批量 insert vs merge</h4>
76467681
auto m1 = m1_init;
76477682
auto m2 = m2_init;
76487683
m2.insert(m1.begin(), m1.end());
7649-
benchmark::DoNotOptimize(m3);
7684+
benchmark::DoNotOptimize(m2);
76507685
}
76517686
}
7652-
BENCHMARK(BM_Insert);
7687+
BENCHMARK(BM_Insert)-&gt;Arg(1000);
76537688

76547689
static void BM_Merge(benchmark::State &amp;state) {
76557690
map&lt;string, int&gt; m1_init;
@@ -7665,7 +7700,7 @@ <h4 id="stl_map-insert-vs-merge">批量 insert vs merge</h4>
76657700
benchmark::DoNotOptimize(m2);
76667701
}
76677702
}
7668-
BENCHMARK(BM_Merge);
7703+
BENCHMARK(BM_Merge)-&gt;Arg(1000);
76697704
</code></pre>
76707705
<p>merge 函数不会产生不必要的内存分配导致内存碎片化,所以更高效。但作为代价,他会清空 m2!</p>
76717706
<ul>
@@ -7777,7 +7812,9 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
77777812
string sex;
77787813

77797814
bool operator&lt;(Student const &amp;that) const {
7780-
return name &lt; that.name || id &lt; that.id || sex &lt; that.sex;
7815+
return x.name &lt; y.name || (x.name == y.name &amp;&amp; (x.id &lt; y.id || (x.id == y.id &amp;&amp; x.sex &lt; y.sex)));
7816+
// 等价于:
7817+
return std::tie(x.name, x.id, y.sex) &lt; std::tie(x.name, x.id, y.sex); // tuple 实现了正确的 operator&lt; 运算符
77817818
}
77827819
};
77837820

@@ -7795,7 +7832,7 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
77957832
template &lt;&gt;
77967833
struct std::less&lt;Student&gt; { // 用户可以特化标准库中的 trait
77977834
bool operator()(Student const &amp;x, Student const &amp;y) const {
7798-
return x.name &lt; y.name || x.id &lt; y.id || x.sex &lt; y.sex;
7835+
return std::tie(x.name, x.id, y.sex) &lt; std::tie(x.name, x.id, y.sex);
77997836
}
78007837
};
78017838

@@ -7815,10 +7852,7 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
78157852

78167853
struct LessStudent {
78177854
bool operator()(Student const &amp;x, Student const &amp;y) const {
7818-
return x.name &lt; y.name || (x.name == y.name &amp;&amp; (x.id &lt; y.id || (x.id == y.id &amp;&amp; x.sex &lt; y.sex)));
7819-
// 等价于:
78207855
return std::tie(x.name, x.id, y.sex) &lt; std::tie(x.name, x.id, y.sex);
7821-
// 因为 tuple 实现了正确的 operator&lt; 运算符
78227856
}
78237857
};
78247858

@@ -7948,8 +7982,8 @@ <h3 id="stl_map-greater">greater 实现反向排序</h3>
79487982
{985, &quot;拳打&quot;},
79497983
{211, &quot;脚踢&quot;},
79507984
};
7951-
map&lt;int, string&gt; m1 = ilist; // 从小到大排序
7952-
map&lt;int, string, greater&lt;int&gt;&gt; m2 = ilist;
7985+
map&lt;int, string&gt; m1 = ilist; // 从小到大排序
7986+
map&lt;int, string, greater&lt;int&gt;&gt; m2 = ilist; // 从大到小排序
79537987
print(m1); // {{211, &quot;脚踢&quot;}, {985, &quot;拳打&quot;}}
79547988
print(m2); // {{985, &quot;拳打&quot;}, {211, &quot;脚踢&quot;}}
79557989
</code></pre>
@@ -8127,9 +8161,9 @@ <h3 id="stl_map-find_1">泛型版的 find 函数</h3>
81278161
<h3 id="stl_map-find_2">泛型 find 的要求:透明</h3>
81288162
<p>要想用泛型版的 find 函数有一个条件:</p>
81298163
<p>map 的比较器必须是“透明(transparent)”的,也就是 <code>less&lt;void&gt;</code> 这种。否则泛型版的 <code>find(Kt &amp;&amp;)</code> 不会参与重载,也就是只能调用传统的 <code>find(K const &amp;)</code>。</p>
8130-
<p>但是 <code>map&lt;K, V&gt;</code> 默认的比较器是 <code>less&lt;V&gt;</code>,他是不透明的,比较的两边必须都是 <code>V</code> 类型。如果其中一边不是的话,就得先隐式转换为 <code>V</code> 才能用。</p>
8164+
<p>但是 <code>map&lt;K, V&gt;</code> 默认的比较器是 <code>less&lt;K&gt;</code>,他是不透明的,比较的两边必须都是 <code>K</code> 类型。如果其中一边不是的话,就得先隐式转换为 <code>K</code> 才能用。</p>
81318165
<p>这是早期 C++98 设计的失败,当时他们没想到 <code>find</code> 还可以接受 <code>string_view</code> 和 <code>const char *</code> 这类可以和 <code>string</code> 比较,但构造会廉价得多的弱引用类型。</p>
8132-
<p>只好后来引入了透明比较器企图,然而为了历史兼容,<code>map&lt;K, V&gt;</code> 默认仍然是 <code>map&lt;K, V, less&lt;K&gt;&gt;</code>。</p>
8166+
<p>只好后来引入了透明比较器企图力挽狂澜,然而为了历史兼容,<code>map&lt;K, V&gt;</code> 默认仍然是 <code>map&lt;K, V, less&lt;K&gt;&gt;</code>。</p>
81338167
<p>如果我们同学的编译器支持 C++14,建议全部改用这种写法 <code>map&lt;K, V, less&lt;&gt;&gt;</code>,从而用上更高效的 find、at、erase、count、contains 等需要按键查找元素的函数。</p>
81348168
<h4 id="stl_map-_59">应用:字符串为键的字典</h4>
81358169
<p>除非传入的刚好就是一个 <code>string</code> 的 const 引用,否则就会发生隐式构造 <code>string</code> 的操作。</p>
@@ -8303,6 +8337,11 @@ <h3 id="stl_map-_63">用途:动态排序!</h3>
83038337

83048338
<h3 id="stl_map-_64">查询某个键对应的多个值</h3>
83058339
<p>因为 multimap 中,一个键不再对于单个值了;所以 multimap 没有 <code>[]</code> 和 <code>at</code> 了,也没有 <code>insert_or_assign</code>(反正 <code>insert</code> 永远不会发生键冲突!)</p>
8340+
<pre><code class="language-cpp">pair&lt;iterator, iterator&gt; equal_range(K const &amp;k);
8341+
8342+
template &lt;class Kt&gt;
8343+
pair&lt;iterator, iterator&gt; equal_range(Kt &amp;&amp;k);
8344+
</code></pre>
83068345
<p>要查询 multimap 中的一个键对应了哪些值,可以用 <code>equal_range</code> 获取一前一后两个迭代器,他们形成一个区间。这个区间内所有的元素都是同样的键。</p>
83078346
<pre><code class="language-cpp">multimap&lt;string, string&gt; tab;
83088347
tab.insert({&quot;rust&quot;, &quot;silly&quot;});

search/search_index.json

+1-1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)