当前位置: 首页 > news >正文

stl 容器新增的实用方法介绍

原位构造与容器的 emplace 系列函数

在介绍emplaceemplace_back方法之前,我们先看一段代码:

#include <iostream> #include <list> class Test { public: Test(int a, int b, int c) { ma = a; mb = b; mc = c; std::cout << "Test constructed." << std::endl; } ~Test() { std::cout << "Test destructed." << std::endl; } Test(const Test& rhs) { if (this == &rhs) return; this->ma = rhs.ma; this->mb = rhs.mb; this->mc = rhs.mc; std::cout << "Test copy-constructed." << std::endl; } private: int ma; int mb; int mc; }; int main() { std::list<Test> collections; for (int i = 0; i < 10; ++i) { Test t(1 * i, 2 * i, 3 * i); collections.push_back(t); } return 0; }

上述代码在一个循环里面产生一个对象,然后将这个对象放入集合当中,这样的代码在实际开发中太常见了。但是这样的代码存在严重的效率问题。循环中的 t 对象在每次循环时,都分别调用一次构造函数、拷贝构造函数和析构函数。这个过程示意如下:

循环 10 次,总共调用三十次。但实际上我们的初衷是创建一个对象 t,将其直接放入集合中,而不是将 t 作为一个中间临时产生的对象,这样的话,总共只需要调用 t 的构造函数 10 次就可以了。有,C++ 11 提供了一个在这种情形下替代 push_back 的方法 ——emplace_back,使用emplace_back,我们将main函数中的代码改写一下:

std::list<Test> collections; for (int i = 0; i < 10; ++i) { collections.emplace_back(1 * i, 2 * i, 3 * i); }

实际执行的时候,我们发现现在,只需要调用 Test 类的构造函数 10 次,大大地提高了执行效率。

同理,在这种情形下,对于像std::liststd::vector这样的容器,其 push/push_front 方法在 C++11 中也有对应的改进方法即 emplace/emplace_front 方法。C++ Reference 上将这里的emplace操作称之为“原位构造元素”(EmplaceConstructible)是非常贴切的。

原方法C++ 11 改进方法方法含义
push/insertemplace在容器指定位置原位构造元素
push_frontemplace_front在容器首部原位构造元素
push_backemplace_back在容器尾部原位构造元素

除了使用 emplace 系列函数原位构造元素,我们也可以为 Test 类添加移动构造函数(Move Constructor)来复用产生的临时对象 t 以提高效率,这将在后面介绍 std::move() 方法时介绍。

std::map 的 try_emplace 与 insert_or_assign 方法

由于 std::map 中的元素的 key 是唯一的,所以在实际开发中我们经常会遇到这样一类需求:即往某个 map 中插入元素时需要先检测 map 中指定的 key 是否存在,如果不存在才做插入操作,如果存在,则直接取来使用;或者在指定 key 不存在时做插入操作,存在时做更新操作。

以 PC 版 QQ 为例,好友列表中每个好友都对应一个 userid,当我们双击某个 QQ 好友头像时,如果与该好友的聊天对话框(这里使用 ChatDialog 表示)已经创建,则直接激活显示,如果不存在,则创建并显示之。假设我们使用 std::map 来管理这些聊天对话框,在 C++17 之前,我们必须编写额外的逻辑去判断元素是否存在,上述逻辑可以编写成如下代码:

class ChatDialog { //其他实现省略... public: void activate() { //实现省略 } }; //用于管理所有聊天对话框的map,key是好友id,ChatDialog是聊天对话框指针 std::map<int64_t, ChatDialog*> m_ChatDialogs; //双击好友头像后 void onDoubleClickFriendItem(int64_t userid) { auto targetChatDialog = m_ChatDialogs.find(userid); //好友对话框不存在,则创建之,并激活 if (targetChatDialog == m_ChatDialogs.end()) { ChatDialog* pChatDialog = new ChatDialog(); m_ChatDialogs.insert(std::pair<int64_t, ChatDialog*>(userid, pChatDialog)); pChatDialog->activate(); } //好友对话框存在,直接激活 else { targetChatDialog->second->activate(); } }

在 C++ 17 中 map 提供了一个try_emplace这样的方法,该方法会检测指定的 key 是否存在,如果存在,则什么也不做。函数签名如下:

template <class... Args> pair<iterator, bool> try_emplace(const key_type& k, Args&&... args); template <class... Args> pair<iterator, bool> try_emplace(key_type&& k, Args&&... args); template <class... Args> iterator try_emplace(const_iterator hint, const key_type& k, Args&&... args); template <class... Args> iterator try_emplace(const_iterator hint, key_type&& k, Args&&... args);

上述函数签名中, 参数k表示需要插入的 key,args 参数是一个不定参数,表示构造 value 对象需要传给构造函数的参数,hint参数可以指定插入位置。

在前两种签名形式中,try_emplace的返回值是一个std::pair<T1, T2>类型,其中T2是一个 bool 类型表示元素是否成功插入 map 中,T1是一个 map 的迭代器,如果插入成功,则返回指向插入位置的元素的迭代器,如果插入失败,则返回 map 中已存在的相同 key 元素的迭代器。 我们用try_emplace改写上面的代码(这里我们不关心插入位置,因此使用前两个签名):

#include <iostream> #include <map> class ChatDialog { //其他实现省略... public: void activate() { //实现省略 } }; //用于管理所有聊天对话框的map,key是好友id,ChatDialog是聊天对话框指针 std::map<int64_t, ChatDialog*> m_ChatDialogs; //普通版本 void onDoubleClickFriendItem(int64_t userid) { auto targetChatDialog = m_ChatDialogs.find(userid); //好友对话框不存在,则创建之,并激活 if (targetChatDialog == m_ChatDialogs.end()) { ChatDialog* pChatDialog = new ChatDialog(); m_ChatDialogs.insert(std::pair<int64_t, ChatDialog*>(userid, pChatDialog)); pChatDialog->activate(); } //好友对话框存在,直接激活 else { targetChatDialog->second->activate(); } } //C++ 17版本1 void onDoubleClickFriendItem2(int64_t userid) { //结构化绑定和try_emplace都是 C++17语法 auto [iter, inserted] = m_ChatDialogs.try_emplace(userid); if (inserted) iter->second = new ChatDialog(); iter->second->activate(); } int main() { //测试用例 //906106643 是userid onDoubleClickFriendItem2(906106643L); //906106644 是userid onDoubleClickFriendItem2(906106644L); //906106643 是userid onDoubleClickFriendItem2(906106643L); return 0; }

使用了try_emplace改写后的代码简洁了许多。但是上述代码存在一个注意事项,由于std::map<int64_t, ChatDialog*> m_ChatDialogs的 value 是指针类型(ChatDialog*),而try_emplace第二个不参数支持的是构造一个 ChatDialog 对象,而不是指针类型,因此,当某个 userid 不存在时,成功插入 map 后会导致相应的 value 为空指针。因此,我们利用inserted的值按需 new 出一个 ChatDialog。当然,新的 C++ 语言规范(C++11 及后续版本)提供了灵活而强大的智能指针以后,我们就不应该再有任何理由去使用裸指针了,因此上述代码可以使用 std::unique_ptr 智能指针类型来重构:

/** * std::map::try_emplace用法演示 * zhangyl 2019.10.06 */ #include <iostream> #include <map> #include <memory> class ChatDialog { //其他实现省略... public: ChatDialog() { std::cout << "ChatDialog constructor" << std::endl; } ~ChatDialog() { std::cout << "ChatDialog destructor" << std::endl; } void activate() { //实现省略 } }; //用于管理所有聊天对话框的map,key是好友id,value是ChatDialog是聊天对话框智能指针 std::map<int64_t, std::unique_ptr<ChatDialog>> m_ChatDialogs; //C++ 17 版本2 void onDoubleClickFriendItem3(int64_t userid) { //结构化绑定和try_emplace都是 C++17语法 auto spChatDialog = std::make_unique<ChatDialog>(); auto [iter, inserted] = m_ChatDialogs.try_emplace(userid, std::move(spChatDialog)); iter->second->activate(); } int main() { //测试用例 //906106643 是userid onDoubleClickFriendItem3(906106643L); //906106644 是userid onDoubleClickFriendItem3(906106644L); //906106643 是userid onDoubleClickFriendItem3(906106643L); return 0; }

上述代码将 map 的类型从std::map<int64_t, ChatDialog>* 改为std::map<int64_t, std::unique_ptr<ChatDialog>>,让程序自动管理聊天对话框对象。程序在 gcc/g++ 7.3 下编译并运行输出如下:

[root@mydev test]# g++ -g -o test_map_try_emplace_with_smartpointer test_map_try_emplace_with_smartpointer.cpp -std=c++17 [root@mydev test]# ./test_map_try_emplace_with_smartpointer ChatDialog constructor ChatDialog constructor ChatDialog constructor ChatDialog destructor ChatDialog destructor ChatDialog destructor

上述代码中构造函数和析构函数均被调用了 3 次,实际上,按最原始的逻辑(上文中普通版本)ChatDialog 应该只被构造和析构 2 次,多出来的一次是因为在try_emplace时,无论某个 userid 是否存在于 map 中均创建一个 ChatDialog 对象(这个是额外的、用不上的对象),由于这个对象并没有被用上,当出了函数onDoubleClickFriendItem3作用域后,智能指针对象spChatDialog被析构,进而导致这个额外的、用不上的 ChatDialog 对象被析构。这相当于做了一次无用功。为此,我们可以继续优化我们的代码如下:

#include <iostream> #include <map> #include <memory> class ChatDialog { //其他实现省略... public: ChatDialog() { std::cout << "ChatDialog constructor" << std::endl; } ~ChatDialog() { std::cout << "ChatDialog destructor" << std::endl; } void activate() { //实现省略 } }; //用于管理所有聊天对话框的map,key是好友id,value是ChatDialog是聊天对话框智能指针 std::map<int64_t, std::unique_ptr<ChatDialog>> m_ChatDialogs; //C++ 17版本3 void onDoubleClickFriendItem3(int64_t userid) { //结构化绑定和try_emplace都是 C++17语法 auto [iter, inserted] = m_ChatDialogs.try_emplace(userid, nullptr); if (inserted) { //这样就按需创建了 auto spChatDialog = std::make_unique<ChatDialog>(); iter->second = std::move(spChatDialog); } iter->second->activate(); } int main() { //测试用例 //906106643 是userid onDoubleClickFriendItem3(906106643L); //906106644 是userid onDoubleClickFriendItem3(906106644L); //906106643 是userid onDoubleClickFriendItem3(906106643L); return 0; }

上述代码我们按照之前的裸指针版本的思路,按需创建一个智能指针对象。这样就避免了一次 ChatDialog 对象无用的构造和析构。再次编译程序,执行结果如下:

[root@mydev test]# g++ -g -o test_map_try_emplace_with_smartpointer2 test_map_try_emplace_with_smartpointer2.cpp -std=c++17 [root@mydev test]# ./test_map_try_emplace_with_smartpointer2 ChatDialog constructor ChatDialog constructor ChatDialog destructor ChatDialog destructor

为了演示try_emplace函数支持原位构造(上文已经介绍),我们将 map 的 value 类型改成 ChatDialog 类型,当然,这里只是为了演示方便,实际开发中对于非 POD 类型的复杂数据类型,在 stl 容器中应该存储其指针或者智能指针类型,而不是对象本身。修改后的代码如下:

#include <iostream> #include <map> class ChatDialog { //其他实现省略... public: ChatDialog(int64_t userid) : m_userid(userid) { std::cout << "ChatDialog constructor" << std::endl; } ~ChatDialog() { std::cout << "ChatDialog destructor" << std::endl; } void activate() { //实现省略 } private: int64_t m_userid; }; //用于管理所有聊天对话框的map,key是好友id,value是ChatDialog是聊天对话框对象 std::map<int64_t, ChatDialog> m_ChatDialogs; //C++ 17版本4 void onDoubleClickFriendItem3(int64_t userid) { //第二个userid是传给ChatDialog构造函数的参数 auto [iter, inserted] = m_ChatDialogs.try_emplace(userid, userid); iter->second.activate(); } int main() { //测试用例 //906106643 是userid onDoubleClickFriendItem3(906106643L); //906106644 是userid onDoubleClickFriendItem3(906106644L); //906106643 是userid onDoubleClickFriendItem3(906106643L); return 0; }

上述代码中,我们为 ChatDialog 类的构造函数增加了一个 userid 参数,因此当我们调用try_emplace方法时,需要传递一个参数,这样try_emplace就会根据 map 中是否已存在同样的 userid 按需构造 ChatDialog 对象。程序执行结果和上一个代码示例应该是一样的:

[root@mydev test]# g++ -g -o test_map_try_emplace_with_directobject test_map_try_emplace_with_directobject.cpp -std=c++17 [root@mydev test]# ./test_map_try_emplace_with_directobject ChatDialog constructor ChatDialog constructor ChatDialog destructor ChatDialog destructor

关于std::move和 智能指针对象std::unique_ptr我们将在后面小节详细介绍。

上面我们介绍了如果 map 中指定的 key 不存在则插入,存在则使用的情形。我们再来介绍一下如果 map 中指定的 key 不存在则插入,存在则更新其 value 值的情形。C++17 为此也为 map 容易新增了一个这样的方法insert_or_assign,让我们不再像 C++17 标准之前,需要额外编写先判断是否存在,不存在则插入,存在则更新的代码了,这次我们可以直接一步到位。insert_or_assign的函数签名如下:

template <class M> pair<iterator, bool> insert_or_assign(const key_type& k, M&& obj); template <class M> pair<iterator, bool> insert_or_assign(key_type&& k, M&& obj); template <class M> iterator insert_or_assign(const_iterator hint, const key_type& k, M&& obj); template <class M> iterator insert_or_assign(const_iterator hint, key_type&& k, M&& obj);

其各个函数参数的含义与try_emplace一样,这里就不再赘述。

我们来看一个例子:

int main() { std::map<std::string, int> mapUsersAge{ { "Alex", 45 }, { "John", 25 } }; mapUsersAge.insert_or_assign("Tom", 26); mapUsersAge.insert_or_assign("Alex", 27); for (const auto& [userName, userAge] : mapUsersAge) { std::cout << "userName: " << userName << ", userAge: " << userAge << std::endl; } }

上述代码中,尝试插入名为Tom的用户,由于该人名在 map 中不存在,因此插入成功;当插入人名为Alex的用户时,由于 map 中已经存在该人名了,因此只对其年龄进行更新,Alex的年龄从 45 更新为 27。程序执行结果如下:

[root@mydev test]# g++ -g -o test_map_insert_or_assign test_map_insert_or_assign.cpp -std=c++17 [root@mydev test]# ./test_map_insert_or_assign userName: Alex, userAge: 27 userName: John, userAge: 25 userName: Tom, userAge: 26

本节介绍了 C++11、17 为 stl 容器新增的几个实用方法,合理利用它们会让我们的程序变得更简洁、更高效。其实新的标准一致在不断改进和优化已有 stl 各个容器,如果读者的工作需要经常与这些容器打交道,建议读者平常留意 C++ 新标准涉及到它们的新动态。

http://www.gsyq.cn/news/1620480.html

相关文章:

  • Selenium WebDriver在.NET 4.8.1 ClickOnce部署中的五大痛点与解决方案
  • 2026年AI写论文软件怎么选?从选题到答辩的全流程攻略
  • 3PEAK思瑞浦 TPA158B5-S5TR-S SOT23-5 电流信号检测放大器
  • XUnity.AutoTranslator:打破语言壁垒的Unity游戏实时翻译架构
  • 工业瑕疵检测项目启动要多久?
  • 当 MCP 把工具接入变成标准动作,科研 Agent 为什么更需要“可调用文档对象”而不只是 Loader
  • 2026最权威AI论文工具榜单:这些被高校和导师悄悄推荐的软件你用对了吗
  • 中兴光猫工厂模式破解:5分钟开启永久Telnet访问权限
  • 2026景德镇黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • Sunshine游戏串流主机:打造你的个人游戏云服务器终极指南
  • AI 图片生成技术解析:扩散模型、多模态与图像编辑的协同机制
  • STM32F207ZG与A5000安全芯片的物联网安全连接方案
  • 如何在单台电脑上实现完美分屏游戏:Nucleus Co-Op完整指南
  • 三月七小助手:你的星穹铁道终极自动化伴侣完整指南
  • Web自动化测试全流程实战:从Selenium到CI/CD集成
  • 【生产环境零容忍】:VMware虚拟机固定IP的7个致命配置错误,第4个导致集群网络中断超47小时
  • 2026支持私有化部署的GEO服务机构盘点 数据安全外贸AI搜索引擎选型指南
  • 企业数据安全合规与电子合同:2026年监管新常态下的必修课
  • 20款论文、文档、音视频内容辅助阅读、分析、摘要生成、内容理解AI工具
  • C++20:Coroutines实践(上):巧用异步文件操作库
  • 2026年盈启鲲鹏数字人直播实测,选这两家最靠谱
  • Si4731 AM/FM收音机芯片与PIC18LF27K42微控制器应用解析
  • 抖音无水印下载神器:三步搞定高清视频保存,告别录屏烦恼
  • 抖音无水印下载器:三步实现免费高清视频批量下载的终极方案
  • paperxie 实操解析|分步骤学术写作工具全拆解,适配各专业论文一站式撰写
  • 2026年企业数字人软件采购避坑最新指南:3个ROI评估核心要点解析
  • VMware虚拟机突然无法识别U盘/加密狗/指纹仪?立即执行这6项关键检查!
  • AD74412R与MKV46F256VLH16工业级信号处理方案解析
  • 为什么你的VMware虚拟机永远跑不满物理资源?——揭秘ESXi NUMA感知、CPU Ready与内存气球三大黑盒
  • 企业 AI 智能体落地:数据、趋势与判断