![C++服务器开发精髓](https://wfqqreader-1252317822.image.myqcloud.com/cover/623/39479623/b_39479623.jpg)
1.10 stl容器新增的实用方法
下面讲解stl容器新增的实用方法。
1.10.1 原位构造与容器的emplace系列函数
在介绍emplace和emplace_back方法之前,我们先看一段代码:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_60_1.jpg?sign=1738811958-B4Khf5wOBGMDVL5y9G08om0MeI5y8GEb-0-6155fea3ae5dc8841027341f309bad91)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_61_1.jpg?sign=1738811958-UmxZUExIh7SSnzkzz0YREd8u1wcMdbm0-0-264f4f54c2503a8e84f8adbb1b8a0435)
以上代码在一个循环里产生一个对象,然后将这个对象放入集合中,这样的代码在实际开发中太常见了。但是这样的代码存在严重的效率问题:循环中的t对象在每次循环时,都分别调用了一次构造函数、拷贝构造函数和析构函数,如下所示。
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_61_2.jpg?sign=1738811958-Fo5u2KGidSKTJv2hYmurbjj2hbSfkuM8-0-8d6734df3c16a5d6b1d953f9b3542305)
以上总共循环10次,调用30次。但实际上,我们的初衷是创建一个对象t,将其直接放入集合中,而不是将t作为一个中间临时产生的对象,这样的话,总共需要调用t的构造函数 10次就可以了。C++11提供了一个在这种情形下替代 push_back 的方法——emplace_back。通过使用emplace_back,可以将main函数中的代码改写如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_61_3.jpg?sign=1738811958-PlwwToJDD7e7e1sPn6dIidJVQZRUtbKc-0-5d3ac6c642277fa727d3d22f613fd626)
经过以上改写,在实际执行时只需调用Test类的构造函数10次,大大提高了执行效率。
同理,在这种情形下,对于像 std::list、std::vector 这样的容器,其 push、push_front方法在C++11中也有对应的改进方法,即emplace/emplace_front方法。在C++Reference上将这里的emplace操作称为“原位构造元素(EmplaceConstructible)”是非常贴切的。
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_61_4.jpg?sign=1738811958-nUiby9o1zSuYnKYwTF3mFAiAnMrZtU4o-0-0d7489ef07da40f50f635ce608722bd7)
除了使用emplace系列的函数原位构造元素,我们也可以为Test类添加移动构造函数(Move Constructor),复用产生的临时对象t以提高效率。
1.10.2 std::map的try_emplace方法与insert_or_assign方法
因为std::map中元素的key是唯一的,所以在实际开发中经常会有这样一类需求:向某个 map中插入元素时需要先检测 map中指定的 key是否存在,不存在时做插入操作,存在时直接取来使用;或者在指定的key不存在时做插入操作,存在时做更新操作。
以PC版的QQ为例,好友列表中的每个好友都对应一个userid,当我们双击某个QQ好友头像时,如果与该好友的聊天对话框(这里使用 ChatDialog 表示)已经存在,则直接将其激活并显示,如果不存在,则将其创建并激活、显示。假设我们使用std::map来管理这些聊天对话框,则在C++17之前的版本中,必须编写额外的逻辑去判断元素是否存在。可以将上述逻辑编写如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_62_1.jpg?sign=1738811958-1wCv6GRS0PWhXMvrGcwZXsxH8LC0aXsP-0-e6421822d5ef2c6191c6984dcaa1a0a5)
在C++17中,map提供了一个try_emplace方法,该方法会检测指定的key是否存在,如果存在,则什么也不做。函数签名如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_62_2.jpg?sign=1738811958-yEhOcNe3pVwUE3qFUbqxp40LXjSwurCN-0-23e72c3b4b8427f3e8efb421f002918a)
在以上函数签名中,参数k表示需要插入的key;args参数是一个不定参数,表示构造value对象需要传给构造函数的参数;通过hint参数可以指定插入的位置。
在前两种签名形式中,try_emplace 的返回值是一个 std::pair<T1,T2>类型,其中 T2是一个 bool 类型,表示元素是否成功插入 map 中;T1 是一个 map 的迭代器,如果插入成功,则返回指向插入位置的元素的迭代器,如果插入失败,则返回 map 中已存在的相同key元素的迭代器。我们用 try_emplace改写上面的代码(这里不关心插入的位置,因此使用前两个签名):
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_63_1.jpg?sign=1738811958-jJhsuoET4dK8UvGTFqkmkZ5DaqA4Fd5R-0-d71a5e893d34bf5529dfa74847b176a5)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_64_1.jpg?sign=1738811958-dnz0r3K9I2AoIP3PkLeuItLiNpmk7iWQ-0-c2c24d37cce367f286b9fbae280b51e1)
使用 try_emplace 改写后的代码简洁了许多。但是在以上代码中需要注意:由于std::map<int64_t,ChatDialog*> m_ChatDialogs 的 value 是指针类型(ChatDialog*),而try_emplace的第 2个参数支持的是构造一个 ChatDialog对象,而不是指针类型,因此在某个 userid 不存在时,成功插入 map 后会导致相应的 value 为空指针。因此,我们利用inserted的值按需新建一个ChatDialog。当然,在新的C++规范(C++11及后续版本)提供了灵活而强大的智能指针以后,我们不应该再有任何理由去使用裸指针了,因此可以对以上代码使用std::unique_ptr智能指针类型来重构:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_64_2.jpg?sign=1738811958-If5V7lQMKJpTor4p9aA0SLwo6ajv4jrG-0-40849044f1610c1ed28dfe929bf2654b)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_65_1.jpg?sign=1738811958-6ae7JnvWYSAN1VCRpiKEpb5mzg4tKIUs-0-15e3c902a6781285e2e45dceca12b2b5)
以上代码将 map 的类型从 std::map<int64_t,ChatDialog*>改为 std::map<int64_t,std::unique_ptr<ChatDialog>>,让程序自动管理聊天对话框对象。程序在gcc/g++7.3中编译并运行输出如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_65_2.jpg?sign=1738811958-ph0jL9m6ah1gyIwmrU5NzXOz0qlT21oN-0-b717f18855ca6e6d02afa36ae7ee6f8d)
在以上代码中,构造函数和析构函数均被调用了3次,实际上,按最原始的逻辑(上文中普通版本)来讲,ChatDialog 应该只被构造和析构 2 次,多出来的一次是因为在try_emplace时,无论某个userid是否存在于map中,均创建一个ChatDialog对象(这是额外的用不上的对象)。由于这个对象并没有被用上,所以在出了 onDoubleClickFriendItem3函数的作用域后,智能指针对象 spChatDialog 被析构,进而导致这个额外的、用不上的ChatDialog对象被析构。这相当于做了一次无用功。为此,我们可以继续优化代码如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_65_3.jpg?sign=1738811958-oScNTxPWQoJ6HL2Umm631VRt0cmDrL6P-0-a58efce7ab3bc058aaea7cbefd19d5eb)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_66_1.jpg?sign=1738811958-pZksHZjEQG1j2sEu7sjEBRiaxcjUt3Pt-0-5bf53ecf925e2b8fcb32fdc1019486c2)
以上代码按照之前裸指针版本的思路,按需创建了一个智能指针对象,避免了一次ChatDialog对象无用的构造和析构。再次编译程序,执行结果如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_66_2.jpg?sign=1738811958-4jLD92K4MHIsKqLbOSjvcOQUYDETVcWE-0-cecba19aaab69a653332442460899b90)
在auto [iter,inserted]=m_ChatDialogs.try_emplace(userid,nullptr);语句中,m_ChatDialogs.try_emplace(userid,nullptr)函数返回两个值,第2个值inserted是一个布尔变量,表示操作是否成功,如果成功,则在第1个返回值iter中含有函数调用成功后的数据。这种函数存在多个返回值且其中一个值表示函数是否调用成功,我们称这种模式为ok-idiom模式,Golang开发者应该很熟悉这种ok-idiom模式。
为了方便验证try_emplace函数支持原位构造(上文已经介绍),我们将map的value类型改成 ChatDialog 类型。在实际开发中,对于非 POD 类型的复杂数据类型,在 stl 容器中应该存储其指针或者智能指针类型,而不是对象本身。修改后的代码如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_67_1.jpg?sign=1738811958-iVtWmPRsNyMyIOMIlsMjD0WklwOpmcH4-0-510814bd224ba6b7ac36c0b07303566c)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_68_1.jpg?sign=1738811958-VY4Z6cZek6omUKh86CdaNu3qwvK3fWaH-0-45d640d5e078bee8fe672041949124b2)
在以上代码中,我们为 ChatDialog 类的构造函数增加了一个 userid 参数,因此当调用 try_emplace 方法时,需要传递一个参数,这样 try_emplace 就会根据 map 中是否已存在同样的 userid 按需构造 ChatDialog 对象了。程序的执行结果和上一个代码示例应该是一样的:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_68_2.jpg?sign=1738811958-5KUXqyhHLWB2P8zI9CPlIy8BYcVjlGf8-0-1119ba6def72c92612f4ad075635ba3e)
对于智能指针对象std::unique_ptr,在后面的小节中将详细介绍。
上文介绍了map中指定的key不存在则插入相应的value,存在则直接使用该key对应的 value的情形。这里再来介绍 map中指定的 key不存在则插入相应的 value,存在则更新其value的情形。C++17为map容器新增了一个insert_or_assign方法,让我们不再像C++17标准之前一样额外编写先判断是否存在,不存在则插入,存在则更新的代码了,这次我们可以一步到位。insert_or_assign的函数签名如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_68_3.jpg?sign=1738811958-aRdfWHP5PCg7LmBRa9LQmeA1uSPjeQeI-0-eb25177e4c3fb829340812e32bcaf8c2)
其各个函数参数的含义与try_emplace一样,这里不再赘述。
再来看一个例子:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_68_4.jpg?sign=1738811958-NMVdcZqvqGOYjqIqeRH5ek2EFcMCnHcf-0-df86064d6ae5108e99f46fb05bd4bbbf)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_69_1.jpg?sign=1738811958-Ru0UcVG2xFg4wAlsr2eH0DL2PX4lZuzX-0-adc4cfe5e694c0afddc1c45bb7ef4b2f)
在以上代码中尝试插入名为Tom的用户,由于该人名在map中不存在,因此插入成功;当插入人名为Alex的用户时,由于在map中已经存在该人名,因此只对其年龄进行更新,将Alex的年龄从45更新为27。程序执行结果如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_69_2.jpg?sign=1738811958-1OvvoV47cOf1JZrYwaEXA8nGtAfbPqTN-0-efc7102ceec197ac0e9ff5059919b2a9)
本节介绍了 C++11/17 为 stl 容器新增的几个实用方法,合理利用这些新增的方法会让我们的程序变得更简洁、高效。其实,新的C++标准一直在不断改进和优化现有的 stl容器,如果经常需要与这些容器打交道,则建议留意C++新标准中这些容器的新动态。