C++中的五种高级初始化技术:从reserve到piecewise_construct等

C++高级初始化技术:reserve、emplace_back、constinit、Lambda表达式、piecewise_construct

  • 一、简介
  • 二、reserve 结合 emplace_back
  • 三、C++ 20的constinit
  • 四、Lambda表达式和初始化
  • 五、make_unique_for_overwrite
  • 六、piecewise_construct 和 forward_as_tuple
  • 七、总结

一、简介

从动态容器操作到编译时常量,C++提供了多种技术。在这篇文章中,将深入研究高级初始化方法,如reserve()emplace_back,以及使用 piecewise_constructforward_as_tuple 构建元组。由于这些技术,我们可以减少临时对象的数量,并更有效地创建变量。

在讲解高级初始化技术之前,我们先看一个前置背景,使用下面的类来方便地说明何时调用它的特殊成员函数。这样,我们就能看到额外的临时对象。

struct MyType {MyType() { std::cout << "MyType default\n"; }explicit MyType(std::string str) : str_(std::move(str)) { std::cout << std::format("MyType {}\n", str_); }~MyType() { std::cout << std::format("~MyType {}\n", str_);  }MyType(const MyType& other) : str_(other.str_) { std::cout << std::format("MyType copy {}\n", str_); }MyType(MyType&& other) noexcept : str_(std::move(other.str_)) { std::cout << std::format("MyType move {}\n", str_);  }MyType& operator=(const MyType& other) { if (this != &other)str_ = other.str_;std::cout << std::format("MyType = {}\n", str_);  return *this;}MyType& operator=(MyType&& other) noexcept { if (this != &other)str_ = std::move(other.str_);std::cout << std::format("MyType = move {}\n", str_);  return *this; }std::string str_;
};

现在我们可以从相对简单但基本的元素开始。

二、reserve 结合 emplace_back

C++中的vector是一种可以根据需要增长的动态数组。但是,每次向量增长超过其当前容量时,它可能需要重新分配内存,这代价是昂贵的。为了优化这一点,可以使用reserve()方法结合emplace_back()

reserve方法不改变vector的大小,但确保vector有足够的已分配内存来存储指定数量的元素。通过提前预留空间,可以防止在向向量添加元素时进行多次重新分配。

示例:

#include <iostream>
#include <string>
#include <vector>
#include <format>struct MyType {MyType() { std::cout << "MyType default\n"; }explicit MyType(std::string str) : str_(std::move(str)) { std::cout << std::format("MyType {}\n", str_); }~MyType() { std::cout << std::format("~MyType {}\n", str_);  }MyType(const MyType& other) : str_(other.str_) { std::cout << std::format("MyType copy {}\n", str_); }MyType(MyType&& other) noexcept : str_(std::move(other.str_)) { std::cout << std::format("MyType move {}\n", str_);  }MyType& operator=(const MyType& other) { if (this != &other)str_ = other.str_;std::cout << std::format("MyType = {}\n", str_);  return *this;}MyType& operator=(MyType&& other) noexcept { if (this != &other)str_ = std::move(other.str_);std::cout << std::format("MyType = move {}\n", str_);  return *this; }std::string str_;
};int main() {    {std::cout << "--- push_back\n";std::vector<MyType> vec;vec.push_back(MyType("First"));std::cout << std::format("capacity: {}\n", vec.capacity());vec.push_back(MyType("Second"));}{std::cout << "--- emplace_back\n";std::vector<MyType> vec;vec.emplace_back("First");std::cout << std::format("capacity: {}\n", vec.capacity());vec.emplace_back("Second");}{std::cout << "--- reserve() + emplace_\n";std::vector<MyType> vec;vec.reserve(2);  // Reserve space for 2 elementsvec.emplace_back("First");vec.emplace_back("Second");}
}

输出:

--- push_back
MyType First
MyType move First
~MyType 
capacity: 1
MyType Second
MyType move Second
MyType move First
~MyType 
~MyType 
~MyType First
~MyType Second
--- emplace_back
MyType First
capacity: 1
MyType Second
MyType move First
~MyType 
~MyType First
~MyType Second
--- reserve() + emplace_
MyType First
MyType Second
~MyType First
~MyType Second

在这个例子中,可以看到三种插入技术之间的比较:

  • push_back()
  • emplace_back()
  • reserve()结合emplace_back

第一种情况下,必须将临时对象传递给push_back,并移动它们来初始化vector的元素。但是还有一个重新分配,因为当添加第二个元素时,vector必须增长。

相对而言,emplace_back()技术更好,更容易编写,因为没有创建临时对象。

但是,第三种选择是最有效的,因为可以预先预留空间,然后在适当的地方创建元素。

通过使用reserveemplace_back,可以确保vector在添加元素到预留容量时不需要重新分配内存。这种组合是优化性能的一种强大方式,特别是在向vector`中添加多个元素时。

三、C++ 20的constinit

constinit是一个强大的工具,用于强制常量初始化,特别是对于静态或线程局部变量。这个关键字在C++ 20中引入,它解决了C++中一个长期存在的难题:静态初始化顺序的问题。通过确保变量在编译时初始化,constinit提供了一个更可预测和更安全的初始化过程。

在其核心,constinit保证它所限定的变量在编译时被初始化。这对全局变量或静态变量尤其有益,可以确保它们不受动态初始化顺序问题的影响。

示例:

#include <array>// 编译期初始化
constexpr int compute(int v) { return v*v*v; }
constinit int global = compute(10);// 这个将不再起作用:
// constinit int another = global;int main() {// 但允许以后更改……global = 100;// global is not constant!// std::array<int, global> arr;
}

在上面的代码中,全局变量是在编译时使用compute函数初始化的。然而,与constconstexpr不同,constinit不会使变量不可变。也就是说,虽然它的初始值是在编译时设置的,但可以在运行时对其进行修改,如main函数所示。此外,由于constinit变量不是constexpr,因此不能使用它初始化另一个constinit对象(如其他的int)。

四、Lambda表达式和初始化

C++ 14对Lambda捕获进行了重大更新,引入了在Lambda捕获子句中直接初始化新数据成员的能力。这个特性称为带有初始化器的捕获或广义Lambda捕获,它在使用Lambda时为我们提供了更大的灵活性和精度。

传统上,Lambda表达式可以从其封闭范围捕获变量。在C++ 14中,现在可以直接在捕获子句中创建和初始化新的数据成员,使Lambdas更加通用。

示例:

#include <iostream>int main() {int x = 30;int y = 12;const auto foo = [z = x + y]() { std::cout << z; };x = 0;y = 0;foo();
}

输出:

42

在这里,创建了一个新的数据成员z,并用xy的和进行初始化。这个初始化发生在Lambda定义点,而不是调用点。因此,即使在Lambda定义之后修改了xy, z仍然保持其初始值。

为了更好地理解这个特性,来看看Lambda是如何转换为可调用类型的:

struct _unnamedLambda {void operator()() const {std::cout << z;}int z;
} someInstance;

Lambda本质上变成了一个带有operator()()方法和数据成员z的未命名结构的实例。

使用初始化器捕获不仅限于简单类型,还可以捕获引用。

这种技术在什么情况会很方便呢?至少有两种情况:

  • 捕获只能移动的类型,特别是通过值。
  • 优化。

考虑第一种情况,下面是捕获std::unique_ptr的方法:

#include <iostream>
#include <memory>int main(){std::unique_ptr<int> p(new int{10});const auto bar = [ptr=std::move(p)] {std::cout << "pointer in lambda: " << ptr.get() << '\n';};std::cout << "pointer in main(): " << p.get() << '\n';bar();
}

在以前的C++ 11中,不能按值捕获惟一的指针,只可能通过引用捕获。现在,从C++ 14开始,可以将对象移动到闭包类型的成员中。

另一个情况是优化。比如,如果捕获一个变量,然后计算一些临时对象:

auto result = std::find_if(vs.begin(), vs.end(),[&prefix](const std::string& s) {return s == prefix + "bars"; });

为什么不计算一次并将其存储在lambda对象中呢?

result = std::find_if(vs.begin(), vs.end(), [savedString = prefix + "bars"](const std::string& s) { return s == savedString; });

这样,savedString只计算一次,而不是每次调用函数时都计算一次。

五、make_unique_for_overwrite

通过使用智能指针,获得了能够显著降低与动态内存分配相关的风险的工具。但是,和所有任何工具一样,总是有改进和优化的空间。

当使用make_unique(或make_shared)分配数组时,默认行为是对每个元素进行值初始化。这意味着对于内置类型,每个元素都设置为零,对于自定义类型,调用其默认构造函数。虽然这确保了内存被初始化为已知状态,但它带来了性能开销,特别是当想要立即覆盖已分配的内存时。

示例:

auto ptr = std::make_unique<int[]>(1000); 

这一行不仅为1000个整数分配内存,而且还将每个整数初始化为零。如果下一步是用来自文件或网络操作的数据填充该内存,那么初始归零是不必要的,也是浪费的。

为了解决这种低效率问题,C++ 20引入了make_unique_for_overwritemake_shared_for_overwrite。这些函数分配内存时不需要对其进行值初始化,这使得它们在直接打算覆盖内存时更快。

auto ptr = std::make_unique_for_overwrite<int[]>(1000);

当分配的内存立即被其他数据覆盖时,*_for_overwrite函数是最有用的。但是要注意,如果内存没有被覆盖,它包含不确定的值,这时如果访问的话可能导致未定义的行为。

这些新功能可以显著提高执行大量内存操作的应用程序的性能,例如数据处理工具或游戏引擎。

六、piecewise_construct 和 forward_as_tuple

最后,让我们看看第五种技术:使用多参数构造函数直接初始化对或元组。这就是std::piecewise_constructstd::forward_as_tuple发挥作用的地方。

示例:

std::pair<MyType, MyType> p { "one", "two" };

上面的代码创建了没有额外临时MyType对象的pair

但是,如果有一个额外的构造函数接受两个参数,那该怎么办呢?

MyType(std::string str, int a)

在这种情况下,如果这样:

std::pair<MyType, MyType> p { "one", 1, "two", 2 };

毫无疑问,它失败了,因为该调用对编译器是二义性的。

在这些情况下,std::piecewise_construct可以提供帮助。它是一个指示std::pair执行分段构造的标记。当与std::forward_as_tuple(创建左值或右值引用的元组)结合使用时,可以将多个参数转发给pair元素的构造函数。

#include <iostream>
#include <string>
#include <format>struct MyType {MyType() { std::cout << "MyType default\n"; }explicit MyType(std::string str) : str_(std::move(str)) { std::cout << std::format("MyType {}\n", str_); }MyType(std::string str, int a) : str_(std::move(str)) { std::cout << std::format("MyType {}, {}\n", str_, a); }~MyType() { std::cout << std::format("~MyType {}\n", str_);  }MyType(const MyType& other) : str_(other.str_) { std::cout << std::format("MyType copy {}\n", str_); }MyType(MyType&& other) noexcept : str_(std::move(other.str_)) { std::cout << std::format("MyType move {}\n", str_);  }MyType& operator=(const MyType& other) { if (this != &other)str_ = other.str_;std::cout << std::format("MyType = {}\n", str_);  return *this;}MyType& operator=(MyType&& other) noexcept { if (this != &other)str_ = std::move(other.str_);std::cout << std::format("MyType = move {}\n", str_);  return *this; }std::string str_;
};int main() {{std::cout << "regular: \n";std::pair<MyType, MyType> p { MyType{"one", 1}, MyType{"two", 2}};}{std::cout << "piecewise + forward: \n";std::pair<MyType, MyType>p2(std::piecewise_construct,std::forward_as_tuple("one", 1),std::forward_as_tuple("two", 2));}
}

运行这个程序会看到以下输出:

regular: 
MyType one, 1
MyType two, 2
MyType move one
MyType move two
~MyType 
~MyType 
~MyType two
~MyType one
piecewise + forward: 
MyType one, 1
MyType two, 2
~MyType two
~MyType one

可以看到,常规方法创建了两个临时对象。而 使用分段选项可以直接将参数传递给pair的元素。

std::piecewise_construct对于像std::mapstd::unordered_map这样存储键值对(std::pair)的容器特别有用。当想要向这些容器中插入元素,并且键或值(或两者)具有多参数构造函数或不可复制时,std::piecewise_construct的实用程序变得很方便。

示例:

#include <string>
#include <map>struct Key {Key(int a, int b) : sum(a + b) {}int sum;bool operator<(const Key& other) const { return sum < other.sum; }
};struct Value {Value(const std::string& s, double d) : name(s), data(d) {}std::string name;double data;
};int main() {std::map<Key, Value> myMap;// doesn't compile: ambiguous// myMap.emplace(3, 4, "example", 42.0);// works:myMap.emplace(std::piecewise_construct,std::forward_as_tuple(3, 4),  std::forward_as_tuple("example", 42.0) );
}

七、总结

本文探讨了初始化C++代码的各种技术。深入研究了现代C++特性的复杂性,包括reserveemplace_back的效率、constinit的准确性和lambda初始化的灵活性。此外,还研究了piecewise forward_as_tuple的细微差别。这些高级技术展示了c++语言的发展和强大,并为开发人员提供了编写更具表现力、更高效和更通用的代码的能力。

有些人可能会认为这是语言中不必要的复杂,这不是一定的。考虑emplace()函数,它可以改进容器插入。但是,如果不需要优化,可以使用更简单的代码来传递临时对象。

所提供的高级技术列表可能并不详尽。博主也非常好奇是否还有其他更有效但更有挑战性的初始化对象的有用技术。可以在评论区分享你的想法。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/2514.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot xxl-job 任务调度

首先官网下载xxl-job的源代码&#xff0c;然后切换到jdk8&#xff0c;等Maven下载依赖 执行mysql的脚本&#xff0c;修改连接配置&#xff0c;启动admin站点 默认地址 http://localhost:8080/xxl-job-admin/ 先新增一个任务执行器&#xff0c;指向未来任务代码的站点 然后在…

探索亚马逊云科技「生成式 AI 精英速成计划」

目录 前言「生成式 AI 精英速成计划」技术开发课程学习课程学习 总结 前言 亚马逊云科技&#xff08;Amazon Web Services&#xff0c;简称AWS&#xff09;作为全球领先的云计算服务提供商&#xff0c;一直以来在推动人工智能&#xff08;AI&#xff09;领域的发展中扮演着重要…

MATLAB将多张小图整合到一张大图形成模板图

MATLAB将多张小图整合到一张大图形成模板图 代码如下: clc;close all;clear all;warning off;%清除变量 rand(seed, 100); randn(seed, 100); format long g;foldername字符模板; [datacell,filenamecell,filenameAllcell]readfun_1n(foldername); K2length(filenamecell);% …

读天才与算法:人脑与AI的数学思维笔记08_生物的创造力

1. 生物的创造力 1.1. 在进化树中是否有其他的物种已经具有与我们人类相当的创造力水平 1.2. 20世纪50年代中期&#xff0c;动物学家德斯蒙德莫里斯&#xff08;Desmond Morris&#xff09;在伦敦动物园做了这样一个试验 1.2.1. 动物学家给…

Laravel 6 - 第十四章 响应

​ 文章目录 Laravel 6 - 第一章 简介 Laravel 6 - 第二章 项目搭建 Laravel 6 - 第三章 文件夹结构 Laravel 6 - 第四章 生命周期 Laravel 6 - 第五章 控制反转和依赖注入 Laravel 6 - 第六章 服务容器 Laravel 6 - 第七章 服务提供者 Laravel 6 - 第八章 门面 Laravel 6 - …

《ESP8266通信指南》4-以Client进行TCP通信(AT指令)

往期 《ESP8266通信指南》3-常用AT指令详解-8266连WIFI-CSDN博客 《ESP8266通信指南》2-ESP8266 AT测试-CSDN博客 《ESP8266通信指南》1-ESP8266 简介-CSDN博客 1. 小节目标 通过 AT 指令使用 8266 进行 TCP 通信 2. 书接上回 复习以下&#xff0c;上一小节我们讲到了 8…

hyperf 三十一 极简DB组件

一 安装及配置 composer require hyperf/db php bin/hyperf.php vendor:publish hyperf/db 默认配置 config/autoload/db.php 如下&#xff0c;数据库支持多库配置&#xff0c;默认为 default。 配置项类型默认值备注driverstring无数据库引擎 支持 pdo 和 mysqlhoststringl…

如何搭建邮箱服务器?mail系统架设的两种方法

邮件mail通信是常用的办公场景&#xff0c;对于技术和网管等人员&#xff0c;往往需要搭建自己的邮箱服务器。那么&#xff0c;如何架设邮箱系统呢&#xff1f;通常有两种方案&#xff0c;一种是在在本地主机部署&#xff0c;另一种是在云端如云服务器上部署应用。根据主机IP情…

立即刷新导致请求的response没有来得及加载造成的this request has no response data available

1、前端递归调用后端接口 const startProgress () > {timer.value setInterval(() > {if (progress.value < 100) {time.value--;progress.value Math.ceil(100 / wait_time.value);} else {clearInterval(timer.value);progress.value 0;timer.value null;time.…

40. UE5 RPG给火球术增加特效和音效

前面&#xff0c;我们将火球的转向和人物的转向问题解决了&#xff0c;火球术可以按照我们的想法朝向目标发射。现在&#xff0c;我们解决接下来的问题&#xff0c;在角色释放火球术时&#xff0c;会产生释放音效&#xff0c;火球也会产生对应的音效&#xff0c;在火球击中目标…

【深度学习】DDoS-Detection-Challenge aitrans2024 入侵检测,基于机器学习(深度学习)判断网络入侵

当了次教练&#xff0c;做了个比赛的Stage1&#xff0c;https://github.com/AItransCompetition/DDoS-Detection-Challenge&#xff0c;得了100分。 一些记录&#xff1a; 1、提交的flowid不能重复&#xff0c;提交的是非入侵的数量和数据flowid,看check.cpp可知。 2、Stage…

大数据入门——概念、工具等

目录 一、基本概念 1.大数据技术 2.大数据特点 3.常见概念 4.数据分析师、数据开发工程师 二、相关工具 三、应用场景 四、大数据业务流程及组织结构 一、基本概念 1.大数据技术 主要解决海量数据的采集、存储和分析计算问题 2.大数据特点 大量、高速、多样、价值、…

【六十】【算法分析与设计】用一道题目解决dfs深度优先遍历,dfs中节点信息,dfs递归函数模板进入前维护出去前回溯,唯一解的剪枝飞升返回值true

路径之谜 题目描述 小明冒充X星球的骑士,进入了一个奇怪的城堡。 城堡里边什么都没有,只有方形石头铺成的地面。 假设城堡地面是nn个方格。如下图所示。 按习俗,骑士要从西北角走到东南角。可以横向或纵向移动,但不能斜着音走,也不能跳跃。每走到一个新方格,就要向正北 方和正西…

ESP32开发

目录 1、简介 1.1 种类 1.2 特点 1.3 管脚功能 1.4 接线方式 1.5 工作模式 2、基础AT指令介绍 2.1 AT指令类型 2.2 基础指令及其描述 2.3 使用AT指令需要注意的事 3、AT指令分类和提示信息 3.1 选择是否保存到Flash的区别 3.2 提示信息 3.3 其他会保存到Flash的A…

基础SQL DQL语句

基础查询 select * from 表名; 查询所有字段 create table emp(id int comment 编号,workno varchar(10) comment 工号,name varchar(10) comment 姓名,gender char(1) comment 性别,age tinyint unsigned comment 年龄,idcard char(18) comment 身份证号,worka…

排序算法:顺序查找

简介 顺序查找&#xff08;也称为线性查找&#xff09;是一种简单直观的搜索算法。按照顺序逐个比较列表或数组中的元素&#xff0c;直到找到目标元素或搜索完整个列表。 应用场景 数据集比较小&#xff0c;无需使用复杂的算法。数据集没有排序&#xff0c;不能使用二分查找…

书生·浦语大模型实战营(第二期):OpenCompass司南大模型评测实战

目录 大语言模型评测中的挑战如何评测大模型模型客观题&主观题提示词工程长文本评测OpenCompass评测流水线CompassHub&#xff1a;高质量评测基准社区 OpenCompass介绍作业&#xff1a;使用OpenCompass评测internlm2-chat-1_8b模型在C-Eval数据集上的性能准备阶段环境配置数…

html--canvas粒子球

<!doctype html> <html> <head> <meta charset"utf-8"> <title>canvas粒子球</title><link type"text/css" href"css/style.css" rel"stylesheet" /></head> <body><script…

element plus:tree拖动节点交换位置和改变层级

图层list里有各种组件&#xff0c;用element plus的tree来渲染&#xff0c;可以把图片等组件到面板里&#xff0c;面板是容器&#xff0c;非容器组件&#xff0c;比如图片、文本等&#xff0c;就不能让其他组件拖进来。 主要在于allow-drop属性的回调函数编写&#xff0c;要理清…

ElasticSearch笔记一

随着这个业务的发展&#xff0c;我们的数据量越来越庞大。那么传统的这种mysql的数据库就渐渐的难以满足我们复杂的业务需求了。 所以在微服务架构下一般都会用到一种分布式搜索的技术。那么今天呢我们就会带着大家去学习分布搜索当中最流行的一种ElasticSearch&#xff0c;Ela…