c/c++开发,无可避免的模板编程实践(篇十)-c++11原位构造元素(emplace)
一、容器修改器的新特性
c++11以前,标准库的容器修改器功能提供了数据插入成员函数inset、push_back,而在 c++11标准化,标准库的容器修改器增加了emplace、emplace_back、emplace_front等插入成员函数。同样是插入函数,两者有何区别呢。
1.1 内存分配器
在c++11中,其实容器的构造及元素空间分配充分利用了std::allocator系列分配器。emplace为原位构造元素,通过 std::allocator_traits::construct 构造元素,用布置 new 在容器提供的位置原位构造元素;将参数 args... 作为 std::forward<Args>(args)... 转发给构造函数。 args... 可以直接或间接地指代容器中的值。
来看一下这些容器的一个类模板定义,都是依赖std::allocator分配器构造的:
//deque
template< class T, class Allocator = std::allocator<T> > class deque;
//list
template< class T, class Allocator = std::allocator<T> > class list;
//map
template< class Key, class T, class Compare = std::less<Key>,class Allocator = std::allocator<std::pair<const Key, T> > > class map;
//set
template< class Key, class Compare = std::less<Key>,class Allocator = std::allocator<Key> > class set;
//.......
c++11标准库提供了各类式分配器。
//分配器,配器是封装内存分配策略的类模板。这允许泛型容器从数据自身将内存管理解耦合。
//定义于头文件 <memory>
allocator //默认的分配器(类模板)
allocator_traits //(C++11) 提供关于分配器类型的信息(类模板)
allocation_result //(C++23) 记录由 allocate_at_least 分配的存储的地址与实际大小(类模板)
allocate_at_least //(C++23) 经由分配器分配至少与请求的大小一样大的存储(函数模板)
allocator_arg_t //(C++11) 标签类型,用于选择具分配器的构造函数重载(类)
allocator_arg //(C++11) 用于选择具分配器的构造函数的 std::allocator_arg_t 对象(常量)
uses_allocator //(C++11) 检查指定的类型是否支持使用分配器的构造(类模板)
uses_allocator_construction_args //(C++20) 准备匹配给定类型所要求的使用分配器构造的口味的参数列表(函数模板)
make_obj_using_allocator //(C++20) 以使用分配器构造的手段创建给类型的对象(函数模板)
uninitialized_construct_using_allocator //(C++20) 以使用分配器构造的手段在指定的内存位置创建给定类型的对象(函数模板) //定义于头文件 <scoped_allocator>
scoped_allocator_adaptor //(C++11) 为多级容器实现的多级分配器(类模板) //定义于头文件 <memory_resource>,定义于命名空间 std::pmr
polymorphic_allocator (C++17) //以 std::memory_resource 构造,支持基于它的运行时多态的分配器(类模板)
std::allocator 类模板是所有标准库容器所用的默认分配器 (Allocator) ,若不提供用户指定的分配器。默认分配器无状态,即任何给定的 allocator 实例可交换、比较相等,且能解分配同一 allocator 类型的任何其他实例所分配的内存。
//std::allocator,定义于头文件 <memory>
template< class T > struct allocator;
template<> struct allocator<void>; //特化,(C++17 中弃用)(C++20 中移除)
现在以std::deque容器为例,来探究一下std::allocator 的用处,首先它是用于为容器所有内存分配实现使用的,可处理容器对内存的分配与释放请求。C++的库的容器其共同特征之一,就是其大小可以在程序的运行时改变;为了实现这一点,进行动态内存分配就显得尤为必要,在此分配器就用于处理容器对内存的分配与释放请求。那么和emplace又有何关系呢
1.2 emplace原位构造元素
std::allocator支持可以移动赋值、构造、插入等操作方式或可复制赋值、构造、插入等操作方式,前者是原来的insert、push_back等处理方式,需要初始化新元素T 的副本,移动 T
进新元素,本质上是采用了operator new分配方式。后者为emplace、emplace_back等处理方式,用布置 new 于容器所提供的位置原位构造元素,即在预分配内存直接构造元素,采用的是std::allocator_traits<Alloc>::construct分配方式。
下来将看这个两种构建元素的方式在内存操作上如何。在这之前了解一下c++11标准里的原子操作的概念,标准库已经倾斜了对原子类型及原子操作的使用比例。
创建test.cpp源文件,输入:
void atomic_optest(void)
{int x = 10;int y = x;x++;y+=1;
}
上述函数的执行语句,那些是原子操作,那些不是呢。
通过汇编输出查看一下,编译命令如下:g++ -S test.cpp -o test.s,和cat test.s:
向一个执行语句只做了一次内存操作,可以认为是原子操作,而int y = x;显然不符合原子操作。
1.3 容器新旧两种修改器方式比对
下来再test.cpp中再定义一个函数:
#include <memory>
void emplace_stest(void)
{int *x = nullptr;std::allocator<int> alloc;alloc.construct(x,10);int *y = ::new int(10);//delete x;x = nullptr;delete y;y = nullptr;
}
再次执行g++ -S test.cpp -o test.s,和cat test.s编译命令如下,可以看出,在调用_ZNSaIiEC1Ev实现alloc定义后,后面给*x构造时,是直接赋值的原子操作,而通过operator new构造元素,在调用_ZNSt15__new_allocatorIiE9constructIiJiEEEvPT_DpOT0_后,还需要进行移动多次原子操作:
1.4 容器旧有修改器函数与emplace修改器效率比对
下来在看第三函数,测试std::deque容器的push_back、push_front、emplace_back、emplace_front的效率,通过1000*100000数据入队列执行步骤做效率比对:
//test.h
#ifndef _TEST_H_
#define _TEST_H_
void emplace_Test(void);
#endif //_TEST_H_//test.cpp
#include <queue>
#include <chrono>
#include <iostream>void emplace_Test(void)
{const unsigned long sizel = 100000; std::deque<int> dque;//push_backauto start = std::chrono::system_clock::now();for (size_t i = 0; i < 1000; i++)for (size_t i = 0; i < sizel; i++){dque.push_back(i%3);}auto end = std::chrono::system_clock::now();std::chrono::duration<double,std::milli> diff = end-start;std::cout << "push_back test diff.count() = " << diff.count() << "ms\n";dque.clear();//emplace_backstart = std::chrono::system_clock::now();for (size_t i = 0; i < 1000; i++)for (size_t i = 0; i < sizel; i++){dque.emplace_back(i%3);}end = std::chrono::system_clock::now();diff = end-start;std::cout << "emplace_back diff.count() = " << diff.count() << "ms\n";dque.clear();//push_frontstart = std::chrono::system_clock::now();for (size_t i = 0; i < 1000; i++)for (size_t i = 0; i < sizel; i++){dque.push_front(i%3);}end = std::chrono::system_clock::now();diff = end-start;std::cout << "push_front diff.count() = " << diff.count() << "ms\n";dque.clear();//emplace_frontstart = std::chrono::system_clock::now();for (size_t i = 0; i < 1000; i++)for (size_t i = 0; i < sizel; i++){dque.emplace_front(i%3);}end = std::chrono::system_clock::now();diff = end-start;std::cout << "emplace_front diff.count() = " << diff.count() << "ms\n";
};
//main.cpp
#include "test.h"int main(int argc, char* argv[])
{emplace_Test();return 0;
}
编译g++ main.cpp test.cpp -o test.exe -std=c++11,运行程序:
可以看到,采用emplace原位构造元素的方式在效率上是略占优势的,这也就是c++11主推emplace方式实现容器修改器的原因,为了保持标准的延续性,又保留了原有的push_back、push_front、insert等成员函数。
二、emplace对容器的支持
下来再次以std::deque看原位构造元素实现原理,std::deque容器提供了emplace、emplace_back、emplace_front三种修改器成员函数,以std::deque<T,Allocator>::emplace为例:
template< class... Args > iterator emplace( const_iterator pos, Args&&... args );
该函数和传统的insert函数实现同样的功能,直接于 pos 前插入元素到容器中。它是通过 std::allocator_traits::construct 构造元素,常用布置 new 在容器提供的位置原位构造元素。然而若要求的位置已被既存的元素占据,则首先在另一位置构造被插入的元素,然后再将他移动赋值到要求的位置中。将参数 args... 作为 std::forward<Args>(args)... 转发给构造函数。 args... 可以直接或间接地指代容器中的值。要注意的是:所有迭代器,含尾后迭代器,都被非法化。引用亦被非法化,除非 pos == begin() 或 pos == end() ,该情况下它们不被非法化。
2.1 容器emplace函数的用法
emplace的用法和insert是一致的,在指定位置前插入元素:
//test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_void emplace_data(void);#endif //_TEST_1_H_
//test1.cpp
#include "test1.h"#include <iostream>
#include <string>
#include <deque>struct A {std::string s;A(std::string str) : s(std::move(str)) { std::cout << " constructed\n"; }A(const A& o) : s(o.s) { std::cout << " copy constructed\n"; }A(A&& o) : s(std::move(o.s)) { std::cout << " move constructed\n"; }A& operator=(const A& other) {s = other.s;std::cout << " copy assigned\n";return *this;}A& operator=(A&& other) {s = std::move(other.s);std::cout << " move assigned\n";return *this;}
};void emplace_data(void)
{std::deque<A> container;std::cout << "construct 2 times A:\n";A two { "two" };A three { "three" };std::cout << "emplace with A&:\n";container.emplace(container.end(), two);std::cout << "emplace:\n";container.emplace(container.begin(), "one");std::cout << "emplace with A&&:\n";container.emplace(container.end(), std::move(three));std::cout << "emplace:\n";container.emplace(std::next(container.begin(),3), "four");std::cout << "content:\n";for (const auto& obj : container)std::cout << ' ' << obj.s;std::cout << '\n';
}
//main.cpp
#include "test1.h"int main(int argc, char* argv[])
{emplace_data();return 0;
}
emplace、emplace_back、emplace_front成员函数还支持next等迭代器操作符给出的指定位置插入数据:
//迭代器操作,定义于命名空间 std::experimental::ranges
advance //令迭代器前进给定的距离(函数模板)
distance //返回迭代器和哨位之间的距离,或范围起始和结尾间的距离(函数模板)
next //自增迭代器(函数模板)
prev //自减迭代器(函数模板)
2.2 emplace对不同容器的支持
针对不同的容器,emplace的成员操作略有不同:
/*相当于原来的insert*/
emplace //顺序容器、关联容器、无序关联容器、variant any
/*在容器中的指定位置后插入新元素。原位构造元素,即不进行复制或移动操作。*/
emplace_after //forward_list
/*相当于原来的push_back*/
emplace_back //deque list vector
/*相当于原来的push_front*/
emplace_front //deque list forward_list
/*插入新元素到容器中尽可能接近于恰在 hint 前的位置(建议性)。原位构造元素,即不进行复制或移动操作*/
emplace_hint //关联容器、无序关联容器
emplace_hint成员函数插入新元素到容器中尽可能接近于恰在 hint 前的位置。原位构造元素,即不进行复制或移动操作。参数args 为转发给元素构造函数的参数,以 std::forward<Args>(args)... 转发调用元素的构造函数。返回指向新插入元素的迭代器,而在因元素已存在而插入失败,则返回指向拥有等价关键的既存元素的迭代器。
template <class... Args> iterator emplace_hint( const_iterator hint, Args&&... args );
emplace_hint成员函数执行复杂度,通常与容器大小成对数,但若新元素正好被插入到 hint 之前则为均摊常数。
//test2.h
#ifndef _TEST_1_H_
#define _TEST_1_H_void emplace_hint_test();#endif //_TEST_1_H_
//test2.cpp
#include "test2.h"#include <chrono>
#include <iostream>
#include <iomanip>
#include <functional>#include <set>
#include <unordered_set>typedef std::set<int> myset;
// typedef std::unordered_set<int> myset;const int nof_operations = 10000000;int set_emplace_hint() {myset set;auto it = set.begin();for(int i = 0; i < nof_operations; ++i) {set.emplace_hint(it, i);it = set.end();}return set.size();
}int set_emplace_hint_wrong() {myset set;auto it = set.begin();for(int i = nof_operations; i > 0; --i) {set.emplace_hint(it, i);it = set.end();}return set.size();
}int set_emplace_hint_corrected() {myset set;auto it = set.begin();for(int i = nof_operations; i > 0; --i) {set.emplace_hint(it, i);it = set.begin();}return set.size();
}int set_emplace_hint_closest() {myset set;auto it = set.begin();for(int i = 0; i < nof_operations; ++i) {it = set.emplace_hint(it, i);}return set.size();
}void timeit(std::function<int()> set_test, std::string what = "") {auto start = std::chrono::system_clock::now();int setsize = set_test();auto stop = std::chrono::system_clock::now();std::chrono::duration<double, std::milli> time = stop - start;if (what.size() > 0 && setsize > 0) {std::cout << std::fixed << std::setprecision(2)<< time.count() << " ms for " << what << '\n';}
}void emplace_hint_test()
{timeit(set_emplace_hint, "emplace with correct hint");timeit(set_emplace_hint_wrong, "emplace with wrong hint");timeit(set_emplace_hint_corrected, "corrected emplace");timeit(set_emplace_hint_closest, "emplace using returned iterator");
};
//main.cpp
#include "test2.h"int main(int argc, char* argv[])
{emplace_hint_test();return 0;
}
编译测试g++ main.cpp test2.cpp -o test.exe -std=c++11,set_emplace_hint_wrong是尾部插入由大到小的数据,因此排序很耗时间:
采用无序关联容器unordered_set测试,调整一下代码:
// typedef std::set<int> myset;
typedef std::unordered_set<int> myset;
再次编译g++ main.cpp test2.cpp -o test.exe -std=c++11测试:
而对于无序容器,元素的插入效率几乎保持一致。
相关文章:

c/c++开发,无可避免的模板编程实践(篇十)-c++11原位构造元素(emplace)
一、容器修改器的新特性 c11以前,标准库的容器修改器功能提供了数据插入成员函数inset、push_back,而在 c11标准化,标准库的容器修改器增加了emplace、emplace_back、emplace_front等插入成员函数。同样是插入函数,两者有何区别呢…...

基于bash通过cdo批处理数据
***#################################### ubuntu中编写shell脚本文件 第一步:用vim创建一个以.sh结尾的文件,此时这个文件是暂时性的文件,当编写好文件并保存时才能看到文件; 第二步:要首先按一下“i”键才能进行插入…...

Map和Set总结
Map和Set Map和Set是专门用来进行搜索的数据结构,适合动态查找 模型 搜索的数据称为关键字(key),关键字对应的叫值(value),key-value键值对 key模型key-value模型 Map存储的就是key-value模型,Set只存储了key Map Map是接口类…...
pytorch网络模型构建中的注意点
记录使用pytorch构建网络模型过程遇到的点 1. 网络模型构建中的问题 1.1 输入变量是Tensor张量 各个模块和网络模型的输入, 一定要是tensor 张量; 可以用一个列表存放多个张量。 如果是张量维度不够,需要升维度, 可以先使用 …...
面试时候这样介绍redis,redis经典面试题
为什么要用redis做缓存 使用Redis缓存有以下几个优点: 1. 提高系统性能:缓存可以将数据存储在内存中,加快数据的访问速度,减少对数据库的读写次数,从而提高系统的性能。 2. 减轻后端压力:使用缓存可以减…...

机械学习 - scikit-learn - 数据预处理 - 2
目录关于 scikit-learn 实现规范化的方法详解一、fit_transform 方法1. 最大最小归一化手动化与自动化代码对比演示 1:2. 均值归一化手动化代码演示:3. 小数定标归一化手动化代码演示:4. 零-均值标准化(均值移除)手动与自动化代码演示&#x…...
华为OD机试题 - 最长连续交替方波信号(JavaScript)| 机考必刷
更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:最长连续交替方波信号题目输入输出示例一输入输出Code解题思路版…...

executor行为相关Spark sql参数源码分析
0、前言 参数名和默认值spark.default.parallelismDefault number of partitions in RDDsspark.executor.cores1 in YARN mode 一般默认值spark.files.maxPartitionBytes134217728(128M)spark.files.openCostInBytes4194304 (4 MiB)spark.hadoop.mapreduce.fileoutputcommitte…...

双通道5.2GSPS(或单通道10.4GSPS)射频采样FMC+模块
概述 FMC140是一款具有缓冲模拟输入的低功耗、12位、双通道(5.2GSPS/通道)、单通道10.4GSPS、射频采样ADC模块,该板卡为FMC标准,符合VITA57.1规范,该模块可以作为一个理想的IO单元耦合至FPGA前端,8通道的JE…...
理解java反射
是什么Java反射是Java编程语言的一个功能,它允许程序在运行时(而不是编译时)检查、访问和修改类、对象和方法的属性和行为。使用反射创建对象相比直接创建对象有什么优点使用反射创建对象相比直接创建对象的主要优点是灵活性和可扩展性。当我…...

EasyRcovery16免费的电脑照片数据恢复软件
电脑作为一种重要的数据储存设备,其中保存着大量的文档,邮件,视频,音频和照片。那么,如果电脑照片被删除了怎么办?今天小编给大家介绍,误删除的照片从哪里可以找回来,误删除的照片如…...

若依微服务版在定时任务里面跨模块调用服务
第一步 在被调用的模块中添加代理 RemoteTaskFallbackFactory.java: package com.ruoyi.rpa.api.factory;import com.ruoyi.common.core.domain.R; import com.ruoyi.rpa.api.RemoteTaskService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springf…...
SpringMVC简单配置
1、pom.xml配置 <dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.1.12.RELEASE</version></dependency></dependencies><build><…...
xcat快速入门工作流程指南
目录一、快速入门指南一、先决条件二、准备管理节点xcatmn.mydomain.com三、第1阶段:添加你的第一个节点并且用带外BMC接口控制它四、第 2 阶段 预配节点并使用并行 shell 对其进行管理二:工作流程指南1. 查找 xCAT 管理节点的服务器2. 在所选服务器上安…...

C++回顾(十九)—— 容器string
19.1 string概述 1、string是STL的字符串类型,通常用来表示字符串。而在使用string之前,字符串通常是 用char * 表示的。string 与char * 都可以用来表示字符串,那么二者有什么区别呢。 2、string和 char * 的比较 (1)…...
Hadoop入门
数据分析与企业数据分析方向 数据是什么 数据是指对可观事件进行记录并可以鉴别的符号,是对客观事物的性质、状态以及相互关系等进行记载的物理符号或这些物理符号的组合,它是可以识别的、抽象的符号。 他不仅指狭义上的数字,还可以是具有一…...

高校如何通过校企合作/实验室建设来提高大数据人工智能学生就业质量
高校人才培养应该如何结合市场需求进行相关专业设置和就业引导,一直是高校就业工作的讨论热点。亘古不变的原则是,高校设置不能脱离市场需求太远,最佳的结合方式是,高校具有前瞻性,能领先市场一步,培养未来…...

提升学习 Prompt 总结
NLP现有的四个阶段: 完全有监督机器学习完全有监督深度学习预训练:预训练 -> 微调 -> 预测提示学习:预训练 -> 提示 -> 预测 阶段1,word的本质是特征,即特征的选取、衍生、侧重上的针对性工程。 阶段2&…...
JavaScript学习笔记(2.0)
BOM--(browser object model) 获取浏览器窗口尺寸 获取可视窗口高度:window.innerWidth 获取可视窗口高度:window.innerHeight 浏览器弹出层 提示框:window.alert(提示信息) 询问框:window.confirm(提示信息) 输…...

直击2023云南移动生态合作伙伴大会,聚焦云南移动的“价值裂变”
作者 | 曾响铃 文 | 响铃说 2023年3月2日下午,云南移动生态合作伙伴大会在昆明召开。云南移动党委书记,总经理葛松海在大会上提到“2023年,云南移动将重点在‘做大平台及生态级新产品,做优渠道转型新动能,做强合作新…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

Linux 文件类型,目录与路径,文件与目录管理
文件类型 后面的字符表示文件类型标志 普通文件:-(纯文本文件,二进制文件,数据格式文件) 如文本文件、图片、程序文件等。 目录文件:d(directory) 用来存放其他文件或子目录。 设备…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...

(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...