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://xiahunao.cn/news/2982184.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

物联网的基本功能及五大核心技术——青创智通

工业物联网解决方案-工业IOT-青创智通 物联网基本功能 物联网的最基本功能特征是提供“无处不在的连接和在线服务”&#xff0c;其具备十大基本功能。 &#xff08;1&#xff09;在线监测&#xff1a;这是物联网最基本的功能&#xff0c;物联网业务一般以集中监测为主、控制为…

Golang | Leetcode Golang题解之第42题接雨水

题目&#xff1a; 题解: func trap(height []int) (ans int) {n : len(height)if n 0 {return}leftMax : make([]int, n)leftMax[0] height[0]for i : 1; i < n; i {leftMax[i] max(leftMax[i-1], height[i])}rightMax : make([]int, n)rightMax[n-1] height[n-1]for i…

Esp32s3固件烧写

芯片图片 烧写完成之后来一段代码,点亮自带的WS2182灯珠 from machine import Pin import neopixel,time# 输出的引脚定义。 pin = Pin(48,Pin.OUT) # 我这块板子上的板载RGB是48脚。可以查看原理图或者直接找个ws2812B灯珠接上正负极和自己定义一个引脚。# 灯珠控制 Int…

安装 Nginx 的三种方式

通过 Nginx 源码安装需要提前准备的内容&#xff1a; GCC 编译器 Nginx 是使用 C 语言编写的程序&#xff0c;因此想要运行 Nginx 就需要安装一个编译工具 GCC 就是一个开源的编译器集合&#xff0c;用于处理各种各样的语言&#xff0c;其中就包含了 C 语言 使用命令 yum i…

电力调度自动化系统由什么构成?

电力调度自动化系统由什么构成&#xff1f; 电力调度自动化系统通过数据采集与传输、数据处理与存储、监视与控制、优化与决策、通信网络和系统应用软件等构成&#xff0c;实现对电力系统的监控、控制和优化。 电力调度自动化系统是一种集成了计算机技术、通信技术、自动化技术…

上位机图像处理和嵌入式模块部署(树莓派4b开机启动脚本)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 编写好程序之后&#xff0c;一般要求程序开机启动后就可以运行。所以这个时候&#xff0c;我们一般就会把程序流程放在开发板的启动脚本当中。如果…

Ubuntu系统开机长

Ubuntu系统开机长 1. 检查开机自启动软件的所占时间2. 将耗时最高的禁止开机自启动 1. 检查开机自启动软件的所占时间 systemd-analyze blame2. 将耗时最高的禁止开机自启动 sudo systemctl disable networking.service这个耗时是有阈值的&#xff0c;一般大于15s的算&#x…

k8s-pod 控制器

文章目录 k8s-pod 控制器无状态服务与有状态服务无状态服务pod 控制器ReplicationController(RC)ReplicaSet(RS)Label 和 Selector Deployment创建滚动更新回滚版本扩容/缩容暂停和恢复 StatefulSet创建扩容/缩容更新RollingUpdate->金丝雀发布OnDelete 删除 DaemonSet节点选…

JumpServer 堡垒机架构

需求背景&#xff1a; 最近由于项目审计需要&#xff0c;要求企业加固应用和系统&#xff0c;顺便加固一些日常使用的软件和系统&#xff0c;远程接入访问安全问题。 需求目的&#xff1a; 部署实施&#xff1a; 1、系统安装 安装执行 curl -sSL https://resource.fit2clou…

Spring 注解开发详解

1. 注解驱动入门案例介绍 1.1 需求描述 1.需求&#xff1a;实现保存一条数据到数据库。 2.表结构&#xff1a;create table account(id int primary key auto_increment,name varchar(50),money double(7,2)); 3.要求&#xff1a;使用spring框架中的JdbcTemplate和DriverMana…

问题记录:交换两行printf -打印结果不同

环境 os: windows IDE: iar toolchain&#xff1a;iar9.32 board: STM32F429 问题描述 同一个float变量&#xff0c;用两行printf打印&#xff0c;先%d打出来&#xff0c;再%.3f打出来&#xff0c;前者输出32&#xff08;正确&#xff09;&#xff0c;后者打出来是0.000。顺…

贝叶斯网络(概念、应用、实例)

目录 一、贝叶斯网络基本概念 1.1主要组成 1.2概率模型 1.3应用场景 1.4推理方法 1.5学习 二、贝叶斯网络在机器学习中的应用 三、应用实例 3.1分类 3.2推荐系统 3.3自然语言处理 一、贝叶斯网络基本概念 贝叶斯网络&#xff0c;也称为信念网络或有向无环图模型&am…

微信小程序开发六(自定义组件)

自定义组件的创建&#xff1a; 如何创建&#xff1a; 右键选择新建component 创建完成之后需要打开app.json&#xff0c;这是全局使用这个组件&#xff0c;想要单独的页面使用&#xff0c;就在当前页面的json文件中定义 "usingComponents": {"my-zj": &quo…

电子邮件免费版有哪些?免费注册电子邮箱

电子邮件有付费版和免费版两种类型&#xff0c;付费版通常具有更大的电子邮箱容量和更强大的电子邮箱功能。但是对于我们个人用户或者是中小型企业来说注册电子邮箱免费版的就够日常使用了。电子邮件的免费版提供商有Zoho Mail、微软、腾讯等&#xff0c;今天我们就来具体了解下…

电子温度计不准需要怎么处理?

电子温度计不准需要怎么处理&#xff1f; 首选将温度计完全浸入温度为0℃左右的水中&#xff0c;使温度计指示值与0℃相等&#xff0c;拿出测量待测物的温度。其次将温度计完全浸入温度为100℃左右的水中&#xff0c;使温度计指示值与100℃相等&#xff0c;拿出测量待测物的温…

丙级资质升级乙级实操:河南灌溉排涝项目所需材料清单

丙级资质升级乙级实操&#xff1a;河南灌溉排涝项目所需材料清单 在河南灌溉排涝项目中&#xff0c;从丙级资质升级到乙级资质是一个重要且复杂的过程。为了成功完成这一过程&#xff0c;需要准备一系列详尽且符合规定的材料。以下是针对此实操所需的关键材料清单&#xff1a;…

产品推荐 | 基于Intel (Altera) Cyclone IV 打造的水星Mercury CA1核心板

01 产品概述 水星Mercury CA1核心板结合了Intel Cyclone IV FPGA、通用接口如USB 2.0和Gigabit Ethernet&#xff0c;具备大量的LVDS I/O、大容量DDR2 SDRAM和大量硬件乘法器&#xff0c;这些使得水星CA1核心板非常适合数字信号处理、网络、高速I/O以及使用Intel NiosII软处理…

Laravel 6 - 第六章 服务容器

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

OpenAI 笔记:chat API

聊天模型接受一系列消息作为输入&#xff0c;并返回模型生成的消息作为输出。 1 导入openai 的api key from openai import OpenAIclient OpenAI(api_key***) 2 调用聊天API completion client.chat.completions.create(model"gpt-3.5-turbo",messages[{"…

TypeScript 中 interface 和 type 的使用#记录

一、interface&#xff1a;接口 interface A{label: string; }const aa ((aObj: A) > {console.log(aObj.label);//123return aObj.label; })aa({label: 123}) 1、可选属性 interface A{label: string;age?: number; } 2、只读属性 interface A{label: string;age?:…