@@ -345,7 +345,7 @@ <h2 id="index-_1">前言</h2>
345
345
<blockquote>
346
346
<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>
347
347
</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>
349
349
<h2 id="index-_2">格式约定</h2>
350
350
<blockquote>
351
351
<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>
5988
5988
<p>你拿着1号朋友家的地址,一发 RPG 导弹把他家炸了。然后你现在突然意识到需要2号朋友家的地址,但是1号朋友家已经被你炸了,你傻乎乎进入燃烧的1号朋友家,被火烧死了。</p>
5989
5989
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ++it) {
5990
5990
m.erase(it);
5991
+ // it 已经失效!
5991
5992
}
5992
5993
</code></pre>
5993
5994
<p>正确的做法是,先进入1号朋友家,安全取出写着2号朋友家地址的字条后,再来一发 RPG 把1号朋友家炸掉。这样才能顺利找到2号朋友家,以此类推继续拆3号……</p>
@@ -6034,7 +6035,6 @@ <h2 id="stl_map-_28">一边遍历一边删除部分元素</h2>
6034
6035
<hr />
6035
6036
<!-- PG109 -->
6036
6037
6037
- <p>::left::</p>
6038
6038
<p>不奔溃</p>
6039
6039
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ) {
6040
6040
auto const &[k, v] = *it;
@@ -6045,7 +6045,6 @@ <h2 id="stl_map-_28">一边遍历一边删除部分元素</h2>
6045
6045
}
6046
6046
}
6047
6047
</code></pre>
6048
- <p>::right::</p>
6049
6048
<p>奔溃</p>
6050
6049
<pre><code class="language-cpp">for (auto it = m.begin(); it != m.end(); ++it) {
6051
6050
auto const &[k, v] = *it;
@@ -6128,8 +6127,8 @@ <h3 id="stl_map-c20-erase_if">C++20 更好的写法:erase_if</h3>
6128
6127
<p>他的参数类型就是刚刚介绍的 <code>value_type</code>,也就是 <code>pair<const K, V></code>。</p>
6129
6128
<p>pair 是一个 STL 中常见的模板类型,<code>pair<K, V></code> 有两个成员变量:</p>
6130
6129
<ul>
6131
- <li>first:V 类型,表示要插入元素的键</li>
6132
- <li>second:K 类型,表示要插入元素的值</li>
6130
+ <li>first:K 类型,表示要插入元素的键</li>
6131
+ <li>second:V 类型,表示要插入元素的值</li>
6133
6132
</ul>
6134
6133
<p>我称之为”键值对”。</p>
6135
6134
<hr />
@@ -6188,15 +6187,13 @@ <h3 id="stl_map-c20-erase_if">C++20 更好的写法:erase_if</h3>
6188
6187
<li>异:当键 K 已经存在时,insert 不会覆盖,默默离开;而 [] 会覆盖旧的值。</li>
6189
6188
</ul>
6190
6189
<p>例子:</p>
6191
- <p>::left::</p>
6192
6190
<pre><code class="language-cpp">map<string, string> m;
6193
6191
m.insert({"key", "old"});
6194
6192
m.insert({"key", "new"}); // 插入失败,默默放弃不出错
6195
6193
print(m);
6196
6194
</code></pre>
6197
6195
<pre><code>{"key": "old"}
6198
6196
</code></pre>
6199
- <p>::right::</p>
6200
6197
<pre><code class="language-cpp">map<string, string> m;
6201
6198
m["key"] = "old";
6202
6199
m["key"] = "new"; // 已经存在?我踏马强行覆盖!
@@ -6384,6 +6381,23 @@ <h3 id="stl_map-insert_1">批量 insert 同样遵循不覆盖原则</h3>
6384
6381
</code></pre>
6385
6382
<pre><code>{"delay": 211, "timeout": 985}
6386
6383
</code></pre>
6384
+ <pre><code class="language-cpp">vector<pair<string, int>> kvs = {
6385
+ {"timeout", 985},
6386
+ {"delay", 211},
6387
+ {"delay", 666},
6388
+ {"delay", 233},
6389
+ {"timeout", 996},
6390
+ };
6391
+ map<string, int> config = {
6392
+ {"timeout", 404},
6393
+ };
6394
+ config.insert(kvs.begin(), kvs.end());
6395
+ print(config);
6396
+
6397
+ vector<unique_ptr<int>> v;
6398
+ </code></pre>
6399
+ <pre><code>{"delay": 211, "timeout": 404}
6400
+ </code></pre>
6387
6401
<!-- PG127 -->
6388
6402
6389
6403
<h3 id="stl_map-insert-map">批量 insert 实现 map 合并</h3>
@@ -6814,8 +6828,9 @@ <h3 id="stl_map-emplace_1">emplace 的原理和优点</h3>
6814
6828
<li>不建议在 map 上使用 emplace/emplace_hint,请改用 try_emplace。</li>
6815
6829
</ul>
6816
6830
<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>
6818
6832
<blockquote>
6833
+ <p><img src="../img/bulb.png" height="30px" width="auto" style="margin: 0; border: none"/> 这是观察到大多是值类型很大,急需就地构造,而键类型没用多少就地构造的需求。例如 <code>map<string, array<int, 1000>></code></p>
6819
6834
<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>
6820
6835
</blockquote>
6821
6836
<p>insert 的托马斯黄金大回旋分奴版:try_emplace(C++17 引入)</p>
@@ -6828,6 +6843,12 @@ <h2 id="stl_map-try_emplace">try_emplace 更好</h2>
6828
6843
<p>他等价于:</p>
6829
6844
<pre><code class="language-cpp">m.insert({key, V(arg1, arg2, ...)});
6830
6845
</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>
6831
6852
<p>由于 emplace 实在是憨憨,他变长参数列表就地构造的是 pair,然而 pair 的构造函数正常不就是只有两个参数吗,变长没有用。实际有用的往往是我们希望用变长参数列表就地构造值类型 V,对 K 部分并不关系。因此 C++17 引入了 try_emplace,其键部分保持 <code>K const &</code>,值部分采用变长参数列表。</p>
6832
6853
<p>我的评价是:这个比 emplace 实用多了,如果要与 vector 的 emplace_back 对标,那么 map 与之对应的一定是 try_emplace。同学们如果要分奴的话还是建议用 try_emplace。</p>
6833
6854
<h3 id="stl_map-try_emplace_1">try_emplace 可以避免移动!</h3>
@@ -6847,9 +6868,9 @@ <h3 id="stl_map-try_emplace_1">try_emplace 可以避免移动!</h3>
6847
6868
m.try_emplace("key", 42); // MyClass(int)
6848
6869
m.try_emplace("key", "hell", 3.14f); // MyClass(const char *, float)
6849
6870
// 等价于:
6850
- m.insert({"key", {} }); // MyClass()
6851
- m.insert({"key", {42} }); // MyClass(int)
6852
- m.insert({"key", { "hell", 3.14f} }); // MyClass(const char *, float)
6871
+ m.insert({"key", MyClass() }); // MyClass()
6872
+ m.insert({"key", MyClass(42) }); // MyClass(int)
6873
+ m.insert({"key", MyClass( "hell", 3.14f) }); // MyClass(const char *, float)
6853
6874
</code></pre>
6854
6875
<p>对于移动开销较大的类型(例如 <code>array<int, 1000></code>),try_emplace 可以避免移动;对于不支持移动构造函数的值类型,就必须使用 try_emplace 了。</p>
6855
6876
<!-- PG146 -->
@@ -6858,6 +6879,7 @@ <h3 id="stl_map-try_emplace_2">谈谈 try_emplace 的优缺点</h3>
6858
6879
<pre><code class="language-cpp">// 以下两种方式效果等价,只有性能不同
6859
6880
m.try_emplace(key, arg1, arg2, ...); // 开销:1次构造函数
6860
6881
m.insert({key, V(arg1, arg2, ...)}); // 开销:1次构造函数 + 2次移动函数
6882
+ m.insert(make_pair(key, V(arg1, arg2, ...))); // 开销:1次构造函数 + 3次移动函数
6861
6883
</code></pre>
6862
6884
<p>但是由于 try_emplace 是用圆括号帮你调用的构造函数,而不是花括号初始化。</p>
6863
6885
<p>导致你要么无法省略类型,要么你得手动定义类的构造函数:</p>
@@ -7388,14 +7410,27 @@ <h4 id="stl_map-_48">用途举例</h4>
7388
7410
<p>调用者稍后可以直接销毁这个特殊智能指针:</p>
7389
7411
<pre><code class="language-cpp">{
7390
7412
auto node = m.extract("fuck");
7391
- print(node.key(), node.value ());
7413
+ print(node.key(), node.mapped ());
7392
7414
} // node 在此自动销毁
7393
7415
</code></pre>
7394
7416
<p>也可以做一些修改后(例如修改键值),稍后重新用 insert(node) 重新把他插入回去:</p>
7395
7417
<pre><code class="language-cpp">auto node = m.extract("fuck");
7396
- nh .key() = "love";
7418
+ node .key() = "love";
7397
7419
m.insert(std::move(node));
7398
7420
</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<string, int> m;
7425
+ auto it = m.find("fuck");
7426
+ assert(it != m.end());
7427
+ // *it 是 pair<const string, int>
7428
+ it->first = "love"; // 错误!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>
7399
7434
<p>或者插入到另一个不同的 map 对象(但键和值类型相同)里:</p>
7400
7435
<pre><code class="language-cpp">// 从 m1 挪到 m2
7401
7436
auto node = m1.extract("fuck");
@@ -7646,10 +7681,10 @@ <h4 id="stl_map-insert-vs-merge">批量 insert vs merge</h4>
7646
7681
auto m1 = m1_init;
7647
7682
auto m2 = m2_init;
7648
7683
m2.insert(m1.begin(), m1.end());
7649
- benchmark::DoNotOptimize(m3 );
7684
+ benchmark::DoNotOptimize(m2 );
7650
7685
}
7651
7686
}
7652
- BENCHMARK(BM_Insert);
7687
+ BENCHMARK(BM_Insert)->Arg(1000) ;
7653
7688
7654
7689
static void BM_Merge(benchmark::State &state) {
7655
7690
map<string, int> m1_init;
@@ -7665,7 +7700,7 @@ <h4 id="stl_map-insert-vs-merge">批量 insert vs merge</h4>
7665
7700
benchmark::DoNotOptimize(m2);
7666
7701
}
7667
7702
}
7668
- BENCHMARK(BM_Merge);
7703
+ BENCHMARK(BM_Merge)->Arg(1000) ;
7669
7704
</code></pre>
7670
7705
<p>merge 函数不会产生不必要的内存分配导致内存碎片化,所以更高效。但作为代价,他会清空 m2!</p>
7671
7706
<ul>
@@ -7777,7 +7812,9 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
7777
7812
string sex;
7778
7813
7779
7814
bool operator<(Student const &that) const {
7780
- return name < that.name || id < that.id || sex < that.sex;
7815
+ return x.name < y.name || (x.name == y.name && (x.id < y.id || (x.id == y.id && x.sex < y.sex)));
7816
+ // 等价于:
7817
+ return std::tie(x.name, x.id, y.sex) < std::tie(x.name, x.id, y.sex); // tuple 实现了正确的 operator< 运算符
7781
7818
}
7782
7819
};
7783
7820
@@ -7795,7 +7832,7 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
7795
7832
template <>
7796
7833
struct std::less<Student> { // 用户可以特化标准库中的 trait
7797
7834
bool operator()(Student const &x, Student const &y) const {
7798
- return x.name < y.name || x.id < y.id || x.sex < y.sex;
7835
+ return std::tie( x.name, x.id, y.sex) < std::tie(x.name, x.id, y.sex) ;
7799
7836
}
7800
7837
};
7801
7838
@@ -7815,10 +7852,7 @@ <h3 id="stl_map-_52">自定义小于号的三种方式</h3>
7815
7852
7816
7853
struct LessStudent {
7817
7854
bool operator()(Student const &x, Student const &y) const {
7818
- return x.name < y.name || (x.name == y.name && (x.id < y.id || (x.id == y.id && x.sex < y.sex)));
7819
- // 等价于:
7820
7855
return std::tie(x.name, x.id, y.sex) < std::tie(x.name, x.id, y.sex);
7821
- // 因为 tuple 实现了正确的 operator< 运算符
7822
7856
}
7823
7857
};
7824
7858
@@ -7948,8 +7982,8 @@ <h3 id="stl_map-greater">greater 实现反向排序</h3>
7948
7982
{985, "拳打"},
7949
7983
{211, "脚踢"},
7950
7984
};
7951
- map<int, string> m1 = ilist; // 从小到大排序
7952
- map<int, string, greater<int>> m2 = ilist;
7985
+ map<int, string> m1 = ilist; // 从小到大排序
7986
+ map<int, string, greater<int>> m2 = ilist; // 从大到小排序
7953
7987
print(m1); // {{211, "脚踢"}, {985, "拳打"}}
7954
7988
print(m2); // {{985, "拳打"}, {211, "脚踢"}}
7955
7989
</code></pre>
@@ -8127,9 +8161,9 @@ <h3 id="stl_map-find_1">泛型版的 find 函数</h3>
8127
8161
<h3 id="stl_map-find_2">泛型 find 的要求:透明</h3>
8128
8162
<p>要想用泛型版的 find 函数有一个条件:</p>
8129
8163
<p>map 的比较器必须是“透明(transparent)”的,也就是 <code>less<void></code> 这种。否则泛型版的 <code>find(Kt &&)</code> 不会参与重载,也就是只能调用传统的 <code>find(K const &)</code>。</p>
8130
- <p>但是 <code>map<K, V></code> 默认的比较器是 <code>less<V ></code>,他是不透明的,比较的两边必须都是 <code>V </code> 类型。如果其中一边不是的话,就得先隐式转换为 <code>V </code> 才能用。</p>
8164
+ <p>但是 <code>map<K, V></code> 默认的比较器是 <code>less<K ></code>,他是不透明的,比较的两边必须都是 <code>K </code> 类型。如果其中一边不是的话,就得先隐式转换为 <code>K </code> 才能用。</p>
8131
8165
<p>这是早期 C++98 设计的失败,当时他们没想到 <code>find</code> 还可以接受 <code>string_view</code> 和 <code>const char *</code> 这类可以和 <code>string</code> 比较,但构造会廉价得多的弱引用类型。</p>
8132
- <p>只好后来引入了透明比较器企图 ,然而为了历史兼容,<code>map<K, V></code> 默认仍然是 <code>map<K, V, less<K>></code>。</p>
8166
+ <p>只好后来引入了透明比较器企图力挽狂澜 ,然而为了历史兼容,<code>map<K, V></code> 默认仍然是 <code>map<K, V, less<K>></code>。</p>
8133
8167
<p>如果我们同学的编译器支持 C++14,建议全部改用这种写法 <code>map<K, V, less<>></code>,从而用上更高效的 find、at、erase、count、contains 等需要按键查找元素的函数。</p>
8134
8168
<h4 id="stl_map-_59">应用:字符串为键的字典</h4>
8135
8169
<p>除非传入的刚好就是一个 <code>string</code> 的 const 引用,否则就会发生隐式构造 <code>string</code> 的操作。</p>
@@ -8303,6 +8337,11 @@ <h3 id="stl_map-_63">用途:动态排序!</h3>
8303
8337
8304
8338
<h3 id="stl_map-_64">查询某个键对应的多个值</h3>
8305
8339
<p>因为 multimap 中,一个键不再对于单个值了;所以 multimap 没有 <code>[]</code> 和 <code>at</code> 了,也没有 <code>insert_or_assign</code>(反正 <code>insert</code> 永远不会发生键冲突!)</p>
8340
+ <pre><code class="language-cpp">pair<iterator, iterator> equal_range(K const &k);
8341
+
8342
+ template <class Kt>
8343
+ pair<iterator, iterator> equal_range(Kt &&k);
8344
+ </code></pre>
8306
8345
<p>要查询 multimap 中的一个键对应了哪些值,可以用 <code>equal_range</code> 获取一前一后两个迭代器,他们形成一个区间。这个区间内所有的元素都是同样的键。</p>
8307
8346
<pre><code class="language-cpp">multimap<string, string> tab;
8308
8347
tab.insert({"rust", "silly"});
0 commit comments