C++:完美转发(二)(std::forward)

一、RVO优化和std::move、std::forward

以下是一个综合性的例子:

#include <iostream>
#include <memory>
#include <ostream>
using namespace std;// 1. 针对右值引用实施std::move,针对万能引用实施std::forward
class Data {};class Widget {std::string name;std::shared_ptr<Data> ptr;public:Widget() {cout << "Widget() used for object@" << this<< " addr_name: " << &(this->name) <<" string buffer addr: "<< static_cast<const void*>(this->name.data())<< endl;};//复制构造函数Widget(const Widget &w) : name(w.name), ptr(w.ptr) {cout << "Widget(const Widget& w) used for object@" << this<< " addr_name: " << &(this->name) <<" string buffer addr: "<< static_cast<const void*>(this->name.data())<< endl;}//针对右值引用使用std::moveWidget(Widget &&rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr)) {cout << "Widget(Widget&& rhs) used for object@" << this<< " addr_name: " << &(this->name) <<" string buffer addr: "<<static_cast<const void*>(this->name.data())<< endl;}//针对万能引用使用std::forward。//注意,这里使用万能引用来替代两个重载版本:void setName(const// string&)和void setName(string&&)//好处就是当使用字符串字面量时,万能引用版本的效率更高。如w.setName("SantaClaus"),此时字符串会被//推导为const// char(&)[11]类型,然后直接转给setName函数(可以避免先通过字量面构造临时string对象)。//并将该类型直接转给name的构造函数,节省了一个构造和释放临时对象的开销,效率更高。template <typename T> void setName(T &&newName) {if (newName != name) { //第1次使用newNamename = std::forward<T>(newName); //针对万能引用的最后一次使用实施forward}}ostream &print_addr_of_name(ostream &os) {cout << "addr of name: " << static_cast<const void*>(this->name.data()) << endl;return os;}
};// 2. 按值返回函数
// 2.1 按值返回的是一个绑定到右值引用的对象
class Complex {double x;double y;public:Complex(double x = 0, double y = 0) : x(x), y(y) {}Complex &operator+=(const Complex &rhs) {x += rhs.x;y += rhs.y;return *this;}
};Complex operator+(Complex &&lhs, const Complex &rhs) //重载全局operator+
{lhs += rhs;return std::move(lhs); //由于lhs绑定到一个右值引用,这里可以移动到返回值上。
}// 2.2 按值返回一个绑定到万能引用的对象
template <typename T> auto test(T &&t) {return std::forward<T>(t); //由于t是一个万能引用对象。按值返回时实施std::forward//如果原对象一是个右值,则被移动到返回值上。如果原对象//是个左值,则会被拷贝到返回值上。
}// 3. RVO优化
// 3.1 返回局部对象
Widget makeWidget() {Widget w;return w; //返回局部对象,满足RVO优化两个条件。为避免复制,会直接在返回值内存上创建w对象。//但如果改成return// std::move(w)时,由于返回值类型不同(Widget右值引用,另一个是Widget)//会剥夺RVO优化的机会,就会先创建w局部对象,再移动给返回值,无形中增加一个移动操作。//对于这种满足RVO条件的,当某些情况下无法避免复制的(如多路返回),编译器仍会默认地对//将w转为右值,即return std::move(w),而无须用户显式std::move!!!
}// 3.2 按值形参作为返回值
Widget makeWidget(Widget w) //注意,形参w是按值传参的。
{return w; //这里虽然不满足RVO条件(w是形参,不是函数内的局部对象),但仍然会被编译器优化。//这里会默认地转换为右值,即return std::move(w)
}int main() {cout << "1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl;Widget w;w.setName("SantaClaus");cout << "w_addr:" << &w << endl;w.print_addr_of_name(cout);cout << "2. 按值返回时" << endl;auto t1 = test(w);auto t2 = test(std::move(w));cout << "t1_addr:" << &t1 << endl;t1.print_addr_of_name(cout);cout << "t2_addr:" << &t2 << endl;t2.print_addr_of_name(cout);cout << "3. RVO优化" << endl;Widget w1 = makeWidget(); //按值返回 局部对象(RVO)cout << "w1_addr:" << &w1 << endl;w1.print_addr_of_name(cout);cout << "w2:\n";Widget w2 = makeWidget(w1); //按值返回 按值形参对象cout << "w2_addr:" << &w2 << endl;w2.print_addr_of_name(cout);return 0;
}

打印结果:

./main
1. 针对右值引用实施std::move,针对万能引用实施std::forward
Widget() used for object@0x16dd46df0 addr_name: 0x16dd46df0 string buffer addr: 0x16dd46df0
w_addr:0x16dd46df0
addr of name: 0x16dd46df0
2. 按值返回时
Widget(const Widget& w) used for object@0x16dd46db8 addr_name: 0x16dd46db8 string buffer addr: 0x16dd46db8
Widget(Widget&& rhs) used for object@0x16dd46d90 addr_name: 0x16dd46d90 string buffer addr: 0x16dd46d90
t1_addr:0x16dd46db8
addr of name: 0x16dd46db8
t2_addr:0x16dd46d90
addr of name: 0x16dd46d90
3. RVO优化
Widget() used for object@0x16dd46d68 addr_name: 0x16dd46d68 string buffer addr: 0x16dd46d68
w1_addr:0x16dd46d68
addr of name: 0x16dd46d68
w2:
Widget(const Widget& w) used for object@0x16dd46d18 addr_name: 0x16dd46d18 string buffer addr: 0x16dd46d18
Widget(Widget&& rhs) used for object@0x16dd46d40 addr_name: 0x16dd46d40 string buffer addr: 0x16dd46d40
w2_addr:0x16dd46d40
addr of name: 0x16dd46d40

需要注意的是,Widget{}中的:

Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr)){cout << "Widget(Widget&& rhs)" << endl;}

这个函数会利用一个右值对象来构造新的对象,其中name()会调用string 的移动构造函数来创建新的 string 对象,这两个对象的地址是不同的,但是 string 指向的缓冲区也就是字符串存储地址是相同的,这点需要注意

string 的移动构造函数:

MyString(MyString&& other) noexcept : data(other.data) {other.data = nullptr;}

但是如果仔细观察,会发现 string.data() 并不会一样,尽管是通过移动构造的,这是因为SSO(小字符串优化)。SSO 的具体阈值取决于 std::string 的实现,通常在 15 到 24 个字符之间。如果字符串长度低于此阈值,字符串内容将存储在对象本身的内部缓冲区中;超过此阈值,则使用动态内存分配。因为事实是,移动构造在某些情况下,并不会比复制构造更高效

当我们将上述示例的字符变长时:

class Widget {std::string name = "SantaClausSantaClausSantaClausSantaClausSantaClausSantaClausSantaClausSantaClaus";...
}
int main() {......Widget w;w.setName("SantaClausSantaClausSantaClausSantaClaus");...
}

编译运行:

./main
1. 针对右值引用实施std::move,针对万能引用实施std::forward
Widget() used for object@0x16f89adf0 addr_name: 0x16f89adf0 string buffer addr: 0x1286066c0
w_addr:0x16f89adf0
addr of name: 0x1286066c0
2. 按值返回时
Widget(const Widget& w) used for object@0x16f89adb8 addr_name: 0x16f89adb8 string buffer addr: 0x128606720
Widget(Widget&& rhs) used for object@0x16f89ad90 addr_name: 0x16f89ad90 string buffer addr: 0x1286066c0
t1_addr:0x16f89adb8
addr of name: 0x128606720
t2_addr:0x16f89ad90
addr of name: 0x1286066c0
3. RVO优化
Widget() used for object@0x16f89ad68 addr_name: 0x16f89ad68 string buffer addr: 0x128606750
w1_addr:0x16f89ad68
addr of name: 0x128606750
w2:
Widget(const Widget& w) used for object@0x16f89ad18 addr_name: 0x16f89ad18 string buffer addr: 0x1286067b0
Widget(Widget&& rhs) used for object@0x16f89ad40 addr_name: 0x16f89ad40 string buffer addr: 0x1286067b0
w2_addr:0x16f89ad40
addr of name: 0x1286067b0

这里的运行结果,符合所有的预期。

二、完美转发失败的情形

(一)完美转发失败

1. 完美转发不仅转发对象,还转发其类型、左右值特征以及是否带有const或volation等修饰词。而完美转发的失败,主要源于模板类型推导失败或推导的结果是错误的类型

2. 实例说明:假设转发的目标函数f,而转发函数为fwd天然就应该是泛型)。函数如下:

template<typename… Ts>
void fwd(Ts&&… params)
{f(std::forward<Ts>(params));
}f(expression);    //如果本语句执行了某操作
fwd(expression);  //而用同一实参调用fwd则会执行不同操作,则称完美转发失败。

(二)五种完美转发失败的情形

1. 使用大括号初始化列表时

(1)失败原因分析:由于转发函数是个模板函数,而在模板类型推导中,大括号初始不能自动被推导为std::initializer_list

(2)解决方案:先用auto声明一个局部变量,再将该局部变量传递给转发函数

2. 0和NULL用作空指针时

(1)失败原因分析:0或NULL以空指针之名传递给模板时,类型推导的结果是整型,而不是所希望的指针类型。

(2)解决方案:传递nullptr,而非0或NULL

3. 仅声明static const 整型成员变量,而无其定义时。

(1)失败原因分析:C++中常量一般是进入符号表的,只有对其取地址时才会实际分配内存。 调用 f 函数时,其实参是直接从符号表中取值,此时不会发生问题。但当调用 fwd 时由于其形参是万能引用,而引用本质上是一个可解引用的指针。 因此当传入 fwd 时会要求准备某块内存以供解引用出该变量出来。但因其未定义,也就没有实际的内存空间, 编译时可能失败(取决于编译器和链接器的实现)。

(2)解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值。

4. 使用重载函数名或模板函数名时

(1)失败原因分析:由于 fwd 是个模板函数,其形参没有任何关于类型的信息。当传入重载函数名或模板函数(代表许许多多的函数)时,就会导致 fwd 的形参不知绑定到哪个函数上。

(2)解决方案:在调用fwd调用时手动为形参指定类型信息

5. 转发位域时

(1)失败原因分析:位域是由机器字的若干任意部分组成的(如32位int的第3至5个比特),但这样的实体是无法直接取地址的。而fwd的形参是个引用,本质上就是指针,所以也没有办法创建指向任意比特的指针。

(2)解决方案:制作位域值的副本,并以该副本来调用转发函数。
  
以下例子分别解释了这些情况:

#include <iostream>
#include <vector>using namespace std;// 1. 大括号初始化列表
void f(const std::vector<int> &v) {cout << "void f(const std::vector<int> & v)" << endl;
}// 2. 0或NULL用作空指针时
void f(int x) { cout << "void f(int x)" << endl; }// 3. 仅声明static const的整型成员变量而无定义
class Widget {public:static const std::size_t MinVals;//仅声明,无定义(因为静态变量需在类外定义!)
};const std::size_t Widget::MinVals = 10;// 4. 使用重载函数名或模板函数名
int f(int (*pf)(int)) {cout << "int f(int(*pf)(int))" << endl;return 0;
}int processVal(int value) { return 0; }
int processVal(int value, int priority) { return 0; }// 5.位域
struct IPv4Header {std::uint32_t version : 4, IHL : 4, DSCP : 6, ECN : 2, totalLength : 16;//...
};template <typename T>
T workOnVal(T param) //函数模板,代表许许多多的函数。
{return param;
}//用于测试的转发函数
template <typename... Ts>
void fwd(Ts &&...param) //转发函数
{f(std::forward<Ts>(param)...); //目标函数
}int main() {cout << "-------------------1. 大括号初始化列表---------------------"<< endl;// 1.1 用同一实参分别调用f和fwd函数f({1, 2, 3}); //{1, 2, 3}会被隐式转换为std::vector<int>// fwd({ 1, 2, 3 });// //编译失败。由于fwd是个函数模板,而模板推导时{}不能自动被推导为std:;initializer_list<T>// 1.2 解决方案auto il = {1, 2, 3};fwd(il);cout << "-------------------2. 0或NULL用作空指针-------------------"<< endl;// 2.1 用同一实参分别调用f和fwd函数// f(NULL);   //调用void f(int)函数,fwd(NULL); // NULL被推导为int,仍调用void f(int)函数// 2.2 解决方案:使用nullptrf(nullptr); //匹配int f(int(*pf)(int))fwd(nullptr);cout << "-------3. 仅声明static const的整型成员变量而无定义--------"<< endl;// 3.1 用同一实参分别调用f和fwd函数f(Widget::MinVals); //调用void f(int)函数。实参从符号表中取得,编译成功!fwd(Widget::MinVals); // fwd的形参是引用,而引用的本质是指针,但fwd使用到该实参时需要解引用//这里会因没有为MinVals分配内存而出现编译失败(取决于编译器和链接器)// 3.2 解决方案:在类外定义该变量cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl;// 4.1 用同一实参分别调用f和fwd函数f(processVal); // ok,由于f形参为int(*pf)(int),带有类型信息,会匹配int// processVal(int value)// fwd(processVal);// //error,fwd的形参不带任何类型信息,不知该匹配哪个processVals重载函数。// fwd(workOnVal);// //error,workOnVal是个函数模板,代表许许多多的函数。这里不知绑定到哪个函数// 4.2 解决方案:手动指定类型信息using ProcessFuncType = int (*)(int);ProcessFuncType processValPtr = processVal;fwd(processValPtr);fwd(static_cast<ProcessFuncType>(workOnVal)); //调用int f(int(*pf)(int))cout << "----------------------5. 转发位域时---------------------" << endl;// 5.1 用同一实参分别调用f和fwd函数IPv4Header ip = {};f(ip.totalLength); //调用void f(int)// fwd(ip.totalLength);// //error,fwd形参是引用,由于位域是比特位组成。无法创建比特位的引用!//解决方案:创建位域的副本,并传给fwdauto length = static_cast<std::uint16_t>(ip.totalLength);fwd(length);return 0;
}

运行结果:

./main
-------------------1. 大括号初始化列表---------------------
void f(const std::vector<int> & v)
void f(const std::vector<int> & v)
-------------------2. 0或NULL用作空指针-------------------
void f(int x)
int f(int(*pf)(int))
int f(int(*pf)(int))
-------3. 仅声明static const的整型成员变量而无定义--------
void f(int x)
void f(int x)
-------------4. 使用重载函数名或模板函数名---------------
int f(int(*pf)(int))
int f(int(*pf)(int))
int f(int(*pf)(int))
----------------------5. 转发位域时---------------------
void f(int x)
void f(int x)

这样修改后,就可以实现完美转发了。

三、参考

这里。

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

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

相关文章

react之Reducer和Context的联合使用

第三章 - 状态管理 使用 Reducer 和 Context 拓展你的应用 Reducer 可以整合组件的状态更新逻辑。Context 可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。 结合使用 reducer 和 context 在 reducer 介绍 的例子里面&#xff0c;状态被 r…

如何修复Windows中的“无Internet,安全”错误?这里有详细步骤

序言 在Windows计算机上连接到互联网非常容易,但是,当你尝试连接到网络时,Windows有时可能会显示消息“无Internet,安全”。此消息的确切含义是什么?如何修复?以下是你需要了解的所有信息。 为什么Windows显示“无Internet,安全”消息 “无Internet,安全”消息是一个…

简约在线生成短网址系统源码 短链防红域名系统 带后台

简约在线生成短网址系统源码 短链防红域名系统 带后台 安装教程&#xff1a;访问 http://你的域名/install 进行安装 源码免费下载地址抄笔记 (chaobiji.cn)https://chaobiji.cn/

图像分割各种算子算法-可直接使用(Canny、Roberts、Sobel)

Canny算子&#xff1a; import numpy as np import cv2 as cv from matplotlib import pyplot as pltimg cv.imread("../test_1_1.png") edges cv.Canny(img, 100, 200)plt.subplot(121),plt.imshow(img,cmap gray) plt.title(Original Image), plt.xticks([]), …

MySQL数据库之UNION 和JOIN连接的区别?

UNION和JOIN连接是用于合并表中数据的两种不同方法。 1、JOIN连接&#xff1a; 用于在查询中将两个或多个表中的行基于它们之间的关联条件进行匹配。JOIN操作允许您将来自不同表的相关数据组合到一起&#xff0c;以便一次性检索所有相关信息。JOIN操作通常涉及使用ON子句指定…

电文加密(C语言)

一、题目说明&#xff1b; 即第1个字母变成第26个字母&#xff0c;第i个字母变成第(26 - i 1)个字母&#xff0c;非字母字符不变。 二、N-S流程图&#xff1b; 三、运行结果&#xff1b; 四、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h&g…

C语言深入理解指针(4)--指针笔试题解析

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 C语言深入理解指针(4) 收录于专栏【C语言学习】 本专栏旨在分享学习C语言学习的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. size…

【机器学习300问】79、Mini-Batch梯度下降法的原理是什么?

Mini-Batch梯度下降法是一种将训练数据集分成小批次进行学习的优化方法&#xff0c;通过这种方式&#xff0c;可以有效地解决内存限制问题并加速学习过程。 一、为什么要使用Mini-Batch&#xff1f; 在机器学习尤其是深度学习中&#xff0c;我们常常面临海量数据处理的问题。如…

吴恩达 深度学习 神经网络 softmax adam 交叉验证

神经网络中的层&#xff1a;输入层&#xff08;layer 0&#xff09;、隐藏层、卷积层&#xff08;看情况用这个&#xff09;、输出层。&#xff08;参考文章&#xff09; 激活函数&#xff1a; 隐藏层一般用relu函数&#xff1b; 输出层根据需要&#xff0c;二分类用sigmoid&…

ExcelVBA在选择区域(有合并)中删除清除空行

【问题】 关于删除空行&#xff0c;以前是用函数来完成工作的&#xff0c; 今天有人提出问题&#xff0c;传来这个文件&#xff0c; 现有数据&#xff0c;1w多行&#xff0c;其中有部分列有不同合并单元格&#xff0c;跨行也不一样。如果要进行筛选删除空行&#xff0c;有一定的…

matlab使用教程(70)—修改坐标区属性

1.控制坐标轴长度比率和数据单位长度 您可以控制 x 轴、y 轴和 z 轴的相对长度&#xff08;图框纵横比&#xff09;&#xff0c;也可以控制一个数据单位沿每个轴的相对长度&#xff08;数据纵横比&#xff09;。 1.1图框纵横比 图框纵横比是 x 轴、y 轴和 z 轴的相对长度。默认…

【二叉树算法题记录】404. 左叶子之和

题目描述 给定二叉树的根节点 root &#xff0c;返回所有左叶子之和。 题目分析 其实这题无论是迭代法还是递归法&#xff0c;最重要的是要明确判断左叶子的条件&#xff1a;当前节点有左孩子&#xff0c;且这个左孩子没有它的左孩子和右孩子。 迭代法 感觉只要二叉树相关…

MySQL查询篇-连接查询

文章目录 inner joinleft join 和right join inner join 内连接是inner join &#xff0c;只返回两个表匹配的数据行。 select a.*,b.* from a inner join b on a.id b.aid; --等价于 select a.*,b.* from a join b on a.id b.aid;left join 和right join 左外连接和右外…

docker部署微服务项目

要部署微服务项目&#xff0c;可以使用Docker来完成。Docker是一种容器化技术&#xff0c;可以将各个微服务打包成独立的容器&#xff0c;并且在同一个Host上运行。 下面是步骤&#xff1a; 安装Docker。根据你的操作系统选择相应的Docker版本&#xff0c;并按照官方文档进行安…

排序算法 下

1.快速排序 快速排序是Hoare在1962年提出的一种二叉树结构的交换排序的方法&#xff0c;采用了递归的思想 思想&#xff1a;任取待排序的元素序列中的某元素作为基准值&#xff0c;按照原来的顺序将序列分为两个子序列&#xff0c; 左子序列中的所有元素均小于基准直&#x…

Python-VBA函数之旅-sum函数

目录 一、sum函数的常见应用场景 二、sum函数使用注意事项 三、如何用好sum函数&#xff1f; 1、sum函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://myelsa1024.blog.csdn.net/ 一、sum函数的常…

CSS:盒子模型

目录 ▐ box—model概述 ▐ 盒子的组成 ▐ 内容区 ▐ 内边距 ▐ 边框 ▐ 外边距 ▐ 清除浏览器默认样式 ▐ box—model概述 • CSS处理网页时&#xff0c;它认为每个标签都包含在一个不可见的盒子里. • 如果把所有的标签都想象成盒子&#xff0c;那么我们对网…

如何判断一个元素是否在可视区域中

可视区域就是我们浏览网页的设备肉眼可见的区域。 在开发总&#xff0c;我们经常需要判断目标元素是否在可视区域内或者可视区域的距离小于一个值&#xff0c;从而实现一些常用的功能&#xff0c;比如&#xff1a; 图片懒加载列表的无限滚动计算广告元素的曝光情况可点击链接…

记一次requests.get()返回数据乱码问题

现象 使用requests.get()请求&#xff0c;添加了header, 返回的数据使用了text接收&#xff1b;打印出来发现是乱码&#xff1b; 尝试解决 1、 设置encoding ret requests.get(url,headersheaders).text ret.encoding utf-8方法不生效&#xff1b; 2、利用apparent_enco…

远程桌面如何连接?

远程桌面连接是一种可以在不同地点之间共享电脑桌面的技术。通过远程桌面连接&#xff0c;用户可以在远程的计算机上操作另一台计算机&#xff0c;就像是直接坐在前者的前面一样。这种技术可以帮助用户解决在不同地点之间共享数据、协同办公、设备管理等问题。 【天联】的使用场…