C++ std::reference_wrapper:让引用更强大

std::reference_wrapper 的通俗易懂解释

  • 一、简介
  • 二、std::reference_wrapper 的初衷
  • 三、常用示例
    • 3.1、与 `make_pair` 和 `make_tuple` 一起使用
    • 3.2、引用容器
    • 3.3、通过 `std::thread` 按引用传递参数给启动函数
    • 3.4、引用作为类成员
    • 3.5、按引用传递函数对象
    • 3.6、与绑定表达式一起使用
  • 四、总结
  • 五、推荐阅读

一、简介

std::reference_wrapper<T> 是一个可复制、可赋值的对象,它模拟了引用(T&)。它提供了引用不可为空的保证,以及类似指针的灵活性,可以重新绑定到另一个对象。

通常使用 std::ref(或 用于 reference_wrapper<const T>std::cref)来创建 std::reference_wrapper<T>

例如:

template<typename N>
void change(N n) {// 如果 n 是 std::reference_wrapper<int>,它会在这里隐式转换为 int&。n += 1; 
}void foo() {int x = 10; int& xref = x;change(xref); // 按值传递// x 仍然是 10std::cout << x << "\n"; // 10// 显式地按引用传递change<int&>(x);// x 现在是 11std::cout << x << "\n"; // 11// 或者使用 std::refchange(std::ref(x)); // 按引用传递// x 现在是 12std::cout << x << "\n"; // 12
}

二、std::reference_wrapper 的初衷

在 C++ 中,尽可能地使用引用而不是指针,因为引用不能为 null,可以明确地表达按引用传递的意图,并且可读性更好,因为没有解引用(*->)的混乱。

但是,引用是对象的顽固别名。这带来了一些挑战,比如上面的例子,函数模板参数类型必须显式指定才能按引用传递。

此外,通常情况下无法获取引用本身的地址,因为根据 C++ 标准,引用不是对象(存储区域)。在 C++ 中,声明对引用的引用、引用的数组以及指向引用的指针都是禁止的。因此,引用类型不满足 STL 容器元素的 Erasable 要求。因此,不能拥有包含引用元素的容器(例如,vectorlist):

std::vector<int&> v; // 错误

此外,引用不能重新绑定到另一个对象。因此,将引用赋值给另一个引用不会赋值引用本身;它会赋值对象:

int x=10, y=20;
int &xref = x, &yref = y;xref = yref;
// xref 仍然引用 x,现在是 20。

由于引用不能重新绑定,因此将引用作为类成员会很麻烦,因为引用成员会使类不可赋值——默认的复制赋值运算符会被删除。移动语义对于引用成员来说根本没有意义。

std::reference_wrapper 是一个可复制、可赋值的对象,它模拟了引用。与它的名字相反,它不包装引用。它通过封装一个指针T*)并隐式转换为引用(T&)来工作。它不能进行默认构造或用临时对象初始化;因此,它不能为 null 或无效:

std::reference_wrapper<int> nr; // 错误!必须初始化。std::string str1{"Hello"};
std::string str2{"World"};
auto r1 = std::ref(str1); // OK
auto r2 = std::ref(str2); // OK
// 赋值会重新绑定 reference_wrapper
r2 = r1;  // r2 现在也引用 str1
// 隐式转换为 std::string&
std::string cstr = r2; // cstr 是 "Hello"// 可以创建 reference_wrapper 的数组
std::reference_wrapper<std::string> arr[] = {str1, str2}; auto r2 = std::ref(std::string("Hello")); // 错误!不允许使用临时对象(右值)。

唯一的缺点是,要访问对象的成员(T),必须使用 std::reference_wrapper<T>::get 方法:

std::string str{"Hello"};auto sref = std::ref(str); 
// 打印 str 的长度
std::cout << sref.get().length() << "\n"; // 5

此外,要为被引用的对象赋值,请使用 get()

sref.get() = "World"; // str 被更改为 "World"
std::cout << str << "\n"; // World

三、常用示例

3.1、与 make_pairmake_tuple 一起使用

std::reference_wrapper 可以用作模板函数(或构造函数)的参数,以避免显式指定模板参数类型。这里一个特殊的例子是 make_pairmake_tuple),它的目的是减少与实例化对(元组)相关的冗长性。比较以下两种方式:

int m=10, n=20;
std::string s{"Hello"};std::pair<int&, int> p1(m, n);
std::tuple<int&, int, std::string&> t1(m, n, s);
// 与以下方式对比
auto p2 = std::make_pair(std::ref(m), n);
auto t2 = std::make_tuple(std::ref(m), n, std::ref(s));

reference_wrapper 的另一个优点是它不能用临时对象实例化。例如,以下代码会导致未定义行为,因为临时对象的生存期仅延长到构造函数参数超出范围为止:

std::string yell() { return "hey"; }std::pair<const std::string&, int> p3(yell(), n); // 错误!
// 临时对象 std::string("hey") 这里已经被销毁。
// 访问 p3.first 这里会导致 UB。

以上情况属于悬空引用,可以使用 reference_wrapper 避免:

// 但是,使用 std::ref 是安全的
auto p4 = std::make_pair(std::ref(yell()), n); // 错误!很好

make_pairmake_tuple 在某种程度上与 C++17 的 无关。但是,这些原因仍然适用,现在,使用构造函数:

由于C++17引入了类模板参数推断 (CTAD),所以make_pairmake_tuple的作用已经不再那么重要了。但是,仍然适用于构造函数:

std::pair p5(std::ref(m), n); std::tuple t3(std::ref(m), n, std::ref(s));

然而,make_pairmake_tuple与CTAD构造的pairtuple之间存在细微的差别(在大多数情况下这并不重要)。make_pairmake_tuplereference_wrapper<T>转换为引用(T&),而CTAD构造的pairtuple则不是这样。

3.2、引用容器

与引用不同,reference_wrapper 是一个对象,因此满足 STL 容器元素的要求(准确地说是 满足Erasable 要求)。因此,reference_wrapper 可以用作向量元素类型。

reference_wrapper<T> 可以作为指针类型(T*)的安全替代方案,用于存储在向量中:

using namespace std;
vector<reference_wrapper<int>> v;  // OK
int a=10;
v.push_back(std::ref(a));

3.3、通过 std::thread 按引用传递参数给启动函数

可以通过 std::thread(startFunction, args) 在创建新线程时将参数传递给启动函数。这些参数按值从线程创建函数传递,因为 std::thread 构造函数会在将参数传递给启动函数之前复制或移动创建者的参数。

因此,启动函数的引用参数不能绑定到创建者的参数。它只能绑定到 std::thread 创建的临时对象:

void start(int& i) { i += 1; }
void start_const(const int& i) { }void create() {int e = 10; // e 在下面被复制到一个临时对象std::thread(start, e).join(); // 错误!不能将临时对象绑定到 int&。std::thread(start_const, e).join(); // OK。但实际上是按值传递
}

如果想按引用将参数传递给启动函数,可以通过 std::ref 来实现,如下所示:

void create() {int e = 10; std::thread(start, std::ref(e)).join(); // OK。按引用传递// e 现在是 11std::thread(start_const, std::ref(e)).join(); // 按引用传递std::thread(start_const, std::cref(e)).join(); // 按引用传递
}

在上面,std::ref 会生成一个 reference_wrapper<int>,它最终会隐式转换为 int&,从而将 start(int&) 的引用参数绑定到 create() 传递的参数。

3.4、引用作为类成员

将引用作为类成员会带来一些问题,例如,它会使类不可赋值,并且实际上不可移动:

struct W {W(int& i):iRef(i) {}int& iRef;
};int u=10, v=20;
W w1(u); 
W w2(v);
w1 = w2; // 错误!隐式删除的复制赋值运算符

通常的做法是避免将引用作为类成员,而是使用指针。

reference_wrapper 提供了二者的最佳组合:

struct W {W(int& i):iRef(i) {}std::reference_wrapper<int> iRef;
};W w3(u); 
W w4(v);
w3 = w4; // OK

3.5、按引用传递函数对象

只要 T 是可调用对象,std::reference_wrapper<T> 就可以像函数一样被调用。如果想避免复制大型或有状态的函数对象,这个特性在 STL 算法中特别有用。

此外,T 可以是任何可调用对象——普通函数、lambda 表达式或函数对象。例如:

struct Large {bool operator()(int i) const {// 过滤和处理return true;}// 大量数据
};const Large large; // 大量不可变数据和函数对象std::vector<int> in1; // 输入向量
std::vector<int> in2; // 输入向量void process() {std::vector<int> out;// 按引用传递 Large 以避免复制std::copy_if(in1.begin(), in1.end(), std::back_inserter(out), std::ref(large));  std::copy_if(in2.begin(), in2.end(), std::back_inserter(out), std::ref(large)); // 使用过滤后的 'out' 向量
}

3.6、与绑定表达式一起使用

std::bind 会生成一个可调用包装器,称为绑定表达式,它会将调用转发到包装的可调用对象。绑定表达式可以绑定包装的可调用对象的部分或全部参数。

但是,绑定参数在绑定表达式中会被复制或移动。

void caw(const std::string& quality, const std::string& food) {std::cout << "A " << quality << " " << food << "\n";
}using namespace std::placeholders;  // for _1, _2std::string donut("donut");
auto donutcaw = std::bind(caw, _1, donut); // donut 被复制
donutcaw("chocolate"); // A chocolate donut

因此,如果想按引用传递绑定参数,需要使用 std::ref(或 std::cref):

std::string muffin("muffin");
auto muffincaw = std::bind(caw, _1, std::ref(muffin)); // muffin 按引用传递
muffincaw("delicious"); // A delicious muffin

四、总结

std::reference_wrapper 是一个强大的工具,它扩展了 C++ 中引用的功能,使其更灵活、更安全。它可以用于:

  • 避免悬空引用: 它不能用临时对象初始化,因此可以有效地防止悬空引用问题。
  • 创建引用容器: std::reference_wrapper 是一个对象,可以作为 STL 容器的元素类型,从而允许创建引用容器。
  • 解决引用作为类成员的难题: std::reference_wrapper 可以作为类成员,使类可赋值,避免了引用成员带来的限制。
  • 按引用传递函数对象: std::reference_wrapper 可以用来按引用传递函数对象,避免了复制大型或有状态的函数对象。
  • 与绑定表达式一起使用: std::reference_wrapper 可以与 std::bind 一起使用,以按引用传递绑定参数。

五、推荐阅读

  • C++ std::reference_wrapper 官方说明(cppreference)。
  • reference_wrapper的提案。

在这里插入图片描述

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

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

相关文章

重新安装 Windows 10/11 后如何恢复丢失的数据?

“嗨&#xff0c;我的 Windows 10/11 崩溃了&#xff0c;所以我不得不重新安装它。我使用 USB 可启动驱动器重新安装了操作系统。但是&#xff0c;重新安装后&#xff0c;C 盘上的所有先前文件都丢失了。有什么方法可以恢复丢失的文件吗&#xff1f;” - 孙雯 在大多数情况下&…

软考 系统架构设计师系列知识点之SOME/IP与DDS(3)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之SOME/IP与DDS&#xff08;2&#xff09; 本文内容参考&#xff1a; 车载以太网 - SOME/IP简介_someip-CSDN博客 https://zhuanlan.zhihu.com/p/369422441 什么是SOME/IP?_someip-CSDN博客 SOME/IP 详解系列&#…

day16--集合进阶(Set、Map集合)

day16——集合进阶&#xff08;Set、Map集合&#xff09; 一、Set系列集合 1.1 认识Set集合的特点 Set集合是属于Collection体系下的另一个分支&#xff0c;它的特点如下图所示 下面我们用代码简单演示一下&#xff0c;每一种Set集合的特点。 //Set<Integer> set ne…

数组-两个升序数组中位数

一、题目描述 二、解题思路 (一).基本思想&#xff1a; 如果列表总长度allsize( arr1.size()arr2.size() ) 为奇数时&#xff0c;中位数位置应该在两个列表排序后的第 allsize/2 位置处&#xff0c;如果allsize为偶数&#xff0c;中位数应该取 (allsize/2)-1 和 allsize/2 的…

【Python】 如何在Python中设置环境变量?

基本原理 在Python中&#xff0c;环境变量是一种存储系统或应用程序配置信息的方式&#xff0c;它们可以被操作系统或应用程序访问。环境变量通常用于配置应用程序的行为&#xff0c;例如指定数据库的连接字符串、API密钥、文件路径等。 Python提供了几种方法来设置和访问环境…

心电信号降噪方法(滤波器/移动平均/小波等,MATLAB环境)

对于一个正常的、完整的心动周期&#xff0c;对应的心电图波形如下图所示&#xff0c;各个波形都对应着心脏兴奋活动的生理过程&#xff0c;包含P波&#xff0c;PR段&#xff0c;QRS波群&#xff0c;ST段&#xff0c;T波&#xff0c;U波。 &#xff08;1&#xff09;P波心电图中…

OpenBMC相关的网站

openbmc官方网站 https://github.com/openbmchttps://github.com/openbmc Dashboard [Jenkins]https://jenkins.openbmc.org/ https://gerrit.openbmc.org/Gerrit Code Reviewhttps://gerrit.openbmc.org/ Searchhttps://grok.openbmc.org/ openbmc参考网站 https://www.c…

R实验 正交试验设计与一元线性回归分析

实验目的&#xff1a; 掌握正交试验设计记号的意义&#xff1b;掌握正交试验设计的直观分析和方差分析&#xff1b;掌握一元线性回归模型的相关概念&#xff1b;掌握最小二乘法的思想&#xff1b;掌握一元线性回归方程的显著性检验和预测。 实验内容&#xff1a; &#xff11;…

C++ day1 作业练习

整理思维导图 定义自己的命名空间my_sapce&#xff0c;在my_sapce中定义string类型的变量s1&#xff0c;再定义一个函数完成对字符串的逆置。 #include <iostream> #include <cstring>using namespace std; namespace my_space {string s1; }void show() {cout<…

NASA数据集——严格校准的臭氧(O3)、甲醛(HCHO)、二氧化碳(CO2)和甲烷(CH4)混合比,以及包括三维风在内的气象数据

Alpha Jet Atmopsheric eXperiment Meteorological Measurement System (MMS) Data 阿尔法喷气式大气实验气象测量系统&#xff08;MMS&#xff09;数据 简介 Alpha Jet Atmospheric eXperiment (AJAX) 是美国国家航空航天局艾姆斯研究中心与 H211, L.L.C. 公司的合作项目&a…

SpringSecurity6从入门到实战之引言和基本概念

SpringSecurity6从入门到实战之引言和基本概念 前言 在当今数字化时代&#xff0c;随着网络应用的日益普及&#xff0c;保护用户数据和系统安全变得至关重要。作为Java开发社区的中坚力量&#xff0c;Spring框架提供了一整套解决方案来构建企业级应用程序。然而&#xff0c;随…

APM2.8飞控

ArduPilotMega 主控可应用于 固定翼、直升机、多旋翼、地面车辆 APM2.8飞控供电有两种 1.电流计供电&#xff0c; 2.带BEC&#xff08;稳压功能&#xff09;的电调供电 ArduPilotMega 内部的硬件结构图&#xff1a; 调试时&#xff0c;不要使用向导&#xff0c;由于向导功能不…

[GDB] GDB调试

目录 一 简介 二 功能: 三 命令: 四 调试准备: 五 开始调试: 5.1 添加断点&#xff1a; 5.2 条件编译 5.3 断点查看 5.4 断点删除: 5.5 查看源码 5.6 单步调试(逐过程)&#xff1a; 5.7 断点调试: 5.8 单步跟踪(逐语句): 5.9 调试过程&#xff1a; 5.9.1 开始调…

在CentOS 8上卸载与安装MySQL 8的详细步骤

关键词&#xff1a;MySQL 8安装、CentOS 8、YUM源配置、卸载MySQL、MySQL残留文件删除、首次登录MySQL临时密码、服务状态检查、MySQL社区服务器 阅读建议&#xff1a;本文适合需要在CentOS 8操作系统上部署最新MySQL 8数据库的系统管理员或开发者阅读。文中步骤简洁清晰&#…

ssm145基于java的电脑硬件库存管理系统+jsp

电脑硬件库存管理系统的设计与实现 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对电脑硬件库存信息管理混乱&…

【设计模式】创建型-抽象工厂模式

前言 在软件开发领域&#xff0c;设计模式是一种被广泛接受的解决方案&#xff0c;用于解决特定问题并提供可维护和可扩展的代码结构。抽象工厂模式&#xff08;Abstract Factory Pattern&#xff09;是其中之一&#xff0c;它提供了一种方法来创建一系列相关或相互依赖的对象…

Linux 使用 yum安装 ELK服务,yum 安装elasticsearch和Kibana(未写完)

文章目录 环境准备ELK组件介绍安装Elasticsearch安装Kibana 丢弃下载ELK 服务安装包Elasticsearch安装 Tips:关闭elasticsearch https 环境准备 ELK组件介绍 ElasticSearch &#xff1a; 是一个近实时&#xff08;NRT&#xff09;的分布式搜索和分析引擎&#xff0c;它可以用…

PyQt6实战 | 绘图画板程序 自由绘制 直线 矩形 椭圆 画笔颜色和大小选择

引言 本文将介绍如何使用 PyQt6 创建一个简单的绘图应用程序。这个应用程序实现了常用的绘图功能&#xff0c;如自由绘制、画直线、矩形和椭圆。此外&#xff0c;还提供了选择画笔颜色、调整画笔宽度、清空画布和导出图像的功能。 环境设置 首先&#xff0c;需要安装 PyQt6&a…

OrangePi AIpro评测 - AI服务篇

0. 环境 ●OrangePi AIpro ●windows电脑 ●路由器 之前我已经对OrangePi AIpro进行了些嵌入式基本操作的评测。接下来进行AI部分。来看看华为昇腾的特别之处。 1.普通CPU和AI CPU 这里请提前用调试串口或者ssh到板子上&#xff0c;记得用户名和密码&#xff0c;分别是HwHiAiUs…

【Mybatis】映射文件获取新增记录的id

我们在讲JDBC的时候讲过在插入新数据值的时候需要获得到自动生成的那个主键id的值 ①获取PreparedStatement的对象的时候 PreparedStatement st conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS ); ②在执行SQL语句后 st.executeUpdate();ResultSet rs st.ge…