C++ 逆向之 forward 函数与完美转发

news/2024/12/28 22:28:17/文章来源:https://www.cnblogs.com/lostin9772/p/18529486

在进行 std::forward 函数的讲解之前,需要知道 std::move 的运行原理,还不是很清楚的朋友建议先看一下前置知识,本次内容是基于 std::move 内容的基础上进行讲解:
C++ 逆向之 move 函数

然后来讲解我们今天的主角:std::forward 函数与完美转发

一、std::forward 函数的作用

std::forward 函数是 C++ 标准库中的一个模板函数,它的主要作用有两个:

  1. 用于在模板代码中转发参数,保持参数的左值或右值语义不变
  2. 这个函数是完美转发(perfect forwarding)的关键部分,它允许函数模板接受任意类型的参数,并将这些参数转发给其他函数,同时保持参数原有的值类别(左值或右值)

光看定义可能感受并不深刻,我们直接上例子,先来看一下不用完美转发的情况:

class MyClass
{
public:// 显式的删除无参构造函数MyClass() = delete;// 有参构造函数MyClass(int value) : value(value){std::cout << "调用了有参构造函数:MyClass(int value)" << std::endl;}// 拷贝构造函数MyClass(const MyClass& other) : value(other.value){std::cout << "调用了拷贝构造函数:MyClass(MyClass& other)" << std::endl;}// 移动构造函数MyClass(MyClass&& other) noexcept : value(other.value){std::cout << "调用了移动构造函数:MyClass(MyClass&& other)" << std::endl;other.value = 0;}// 析构函数~MyClass(){std::cout << "调用了析构函数:~MyClass()" << std::endl;}private:int value;
};template <typename T>
MyClass* Creator(T&& other)
{return new MyClass(other);
}int main()
{MyClass myClass1(10);MyClass* myClass2 = Creator(myClass1);MyClass* myClass3 = Creator(std::move(myClass1));delete myClass2;delete myClass3;return 0;
}

在上面的代码中,我们构造了一个类,为这个类分别配备了有参构造函数、拷贝构造函数和移动构造函数,我们知道拷贝构造函数是会发生内存拷贝的,对于大型的对象来说会有很大的性能开销,而移动构造函数不会发生值拷贝现象。

此外,我们还添加了一个函数模板,用于传入一个类,并重新构造一个新的类进行返回。

在主函数中,我们首先利用有参构造函数实例化了一个对象 myClass1,接下来我们将 myClass1 作为左值传入了模板,然后调用拷贝构造函数,最后,我们将 myClass1 通过 std::move 函数转化为右值作为参数传递给模板,我们想要这个模板调用移动构造函数,避免性能上的开销,实际上结果怎么样呢?如下:

我们看到,虽然我们传入了一个右值,但是还是调用了拷贝构造函数,这是为什么呢?我们在分析 std::move 函数的时候说过,myClass1 会被当做左值传递,然后我们可以利用 std::move 将其转化为右值进行参数传递,那么在函数里面 myClass 就是右值,但是这里为什么会当做左值并调用拷贝构造函数呢?

其实我们之前分析的没问题,在这个例子中,myClass1 被作为右值参数传进去的时候,在模板函数体里面还是右值,但是!模板函数里面还调用了构造函数return new MyClass(other);,相当于这个右值又被当参数传给了一个新的函数体,那么这个右值会被强制转为左值,传递给return new MyClass(other);,所以最终调用了拷贝构造函数,造成了性能上的开销。

二、完美转发

那么有没有方法避免上面的情况呢?我们想要的结果是传进去的是左值,不管在模板里面嵌套多少个函数体,一直保持左值这个语义不丢失,右值同理,那么我们就需要用到 std::forward 函数,构造一个完美转发,这个函数就是为了解决这个问题而诞生的,我们将模板函数进行修改:

template <typename T>
MyClass* Creator(T&& other)
{return new MyClass(std::forward<T>(other));
}

我们在模板最外层的函数体里面,参数还是右值语义没有丢失,如果想要继续保持右值语义,那么我们就需要在传入模板里面嵌套函数的时候,通过 std::forward 函数进行转发,那么在嵌套的子函数里面仍然会保持右值语义,左值同理。

我们来看在通过 std::forward 构造了完美转发后的运行的结果:

这个时候我们就实现了,在子函数里面,myClass1 仍然被当做右值,并且调用了移动构造函数,不会产生额外的性能开销,这就是std::forward完美转发 存在的意义!

三、逆向 std::forward 函数

那么在了解完用法后,感兴趣的朋友可以继续跟我来看看底层的实现原理,其实和 std::move 大同小异,我们先扒出vs 中 std::forward 的底层代码:

template <class _Ty>
struct remove_reference {using type = _Ty;    // 如果参数是非引用类型,则直接返回非引用类型本身 _Tyusing _Const_thru_ref_type = const _Ty;
};template <class _Ty>
struct remove_reference<_Ty&> {using type = _Ty;    // 如果参数是左值引用,则移除左值引用,返回其底层的非引用类型(参数为 int& 则返回 int)using _Const_thru_ref_type = const _Ty&;
};template <class _Ty>
struct remove_reference<_Ty&&> {using type = _Ty;    // 如果参数是右值引用,则移除右值引用,返回其底层的非引用类型(参数为 int&& 也返回 int)using _Const_thru_ref_type = const _Ty&&;
};template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;    // 移除左值或右值引用,返回其底层的非引用类型template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);
}template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvaluestatic_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");return static_cast<_Ty&&>(_Arg);template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movablereturn static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

关于 remove_reference 这里不再赘述,不懂的朋友请看 std::move 那一节,里面有详细的介绍。

我们看 std::forwardstd::move 的定义非常的像,不同的地方是 std::forward 有两个特例版本,一个接收左值引用,一个接受右值引用,而 std::move 用的是一个万能引用 _Ty&&

我们来看,当我们模板里面传入一个左值或左值引用的时候,_Ty=MyClass&,那么就会调用 std::forward 的第一个特例版本并返回:return static_cast<_Ty&&>(_Arg); => return static_cast<MyClass&&&>(_Arg); => 发生引用折叠:return static_cast<MyClass&>(_Arg);,最终返回一个左值引用。

当我们模板里面传入一个右值引用的时候,_Ty=MyClass&&,那么就会调用 std::forward 的第二个特例版本并返回:return static_cast<_Ty&&>(_Arg); => return static_cast<MyClass&&&&>(_Arg); => 发生引用折叠:return static_cast<MyClass&&>(_Arg);,最终返回一个右值引用。

因此,std::forward 函数通过这种方式,可以在模板中进行参数传递的时候保持左值或右值的语义不发生变化,从而实现完美转发的功能。

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

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

相关文章

中电金信:企业数据赋能效果差,科学试错体系了解一下?

​ Wuhu,咨询专题第五期内容来啦~ 继先后讲解了企业数字化转型 过程中的价值创造、运营变革 以及平台化建设等难题如何解决后本期我们一起来关注 企业科学试错体系构建事情是这样的 👇 👇 👇 随着金融数字化转型的深入推进,以大数据为基础的智能化应用大量涌现,使得数…

【华为笔试-3】HJ20 密码验证合格程序

【华为笔试-3】HJ20 密码验证合格程序HJ20 密码验证程序输入: 021Abc9000 021Abc9Abc1 021ABC9000 021$bc9000输出: OK NG NG OK注:输入结束后有中止结束标志EOF【这个原题给的时候无说明,自己跑他的用例和看论坛看出来的】题解: 要点有三个,分别是:长度超过八位、包含三…

记Linux使用异常2

麒麟v10系统开机后提示如下信息,并进入initramfs模式,[0.224166][ 0l dmi: Firmware registration failed, [0.936010][ 0] serial8250 serial8250.0: unable to register port at index 1 (IOFFFFF10204000000 MEM0 IRQ0): -22 [1.829735][ 0] i8042: i8042 controller self…

SVN集成ExcelMerge

在SVN中对比表格差异时,如果只是一个sheet的CSV表格,SVN自带的diff效果也很好,如果是多个Sheet的xlsx表格,,SVN自带的diff效果就很差 ExcelMerge是一个在Windows平台下比对Excel的工具,当Excel存在多个Sheet时也能很好的支持差异的比对,配合上svn可以很好的查看策划的数…

homeassistant docker搭建并接入米家设备

homeassistant docker搭建教程: 下载镜像 docker pull homeassistant/home-assistant:latest 找个顺眼的地方建立目录,如/etc/dockerdata/homeassistant创建容器 mkdir /etc/dockerdata mkdir /etc/dockerdata/homeassistant然后执行命令: docker run -d \--name homeassist…

例题7.3

import numpy as np from scipy.interpolate import lagrange import matplotlib.pyplot as plt import matplotlib yx = lambda x: 1/(1+x**2)def fun(n):x = np.linspace(-5, 5, n+1)p = lagrange(x, yx(x)) # n次插值多项式return px0 = np.linspace(-5, 5, 100)plt.rc(fo…

Windows10 移动热点无法连接

一:windows10 移动热点手机无法连接 1.查看原因手机连接不上,看看是不是一直在现实获取ip地址2.网卡设置当打开移动热点时候,会有一张网卡 名称为 Microsoft Wi-Fi xxxxx打开你的上网网卡-属性->共享(右上角)->开启连接共享->选择Microsoft Wi-Fi 那张网卡连接成…

解决修改php.ini配置文件不生效的问题

公司禅道研发反馈任务创建的多了就报:Allowed memory size of 33554432 bytes exhausted (tried to alloate 3643528 bytes) in lib/base/front/front.class.php on line 1315 when visiting /index.php?m=execution&f=task&id=28;通过报错即可得知,php的memory_li…

Error:Kotlin: Module was compiled with an incompatible version of Kotlin.

idea 启动项目时报错 Error:Kotlin: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.1.16. 解决方案(windows): 1、打开Kotlin面板 路径:File -> settings -> Languages & F…

JavaScript用法

JavaScript 用法HTML 中的 Javascript 脚本代码必须位于 <script> 与 </script> 标签之间。 Javascript 脚本代码可被放置在 HTML 页面的 <body> 和 <head> 部分中。<script> 标签 如需在 HTML 页面中插入 JavaScript,请使用 <script> 标…

在vite里面,使用linaria,css样式名混淆的问题

我们项目使用css in js来实现样式,借用了一个插件linaria。但是有一个问题,就是样式名会被混淆如下解决方法是,vite配置里面加一个 就可以了,结果如下

刚毕业,去做边缘业务,还有救吗?

有一些同时拿到了多个 Offer 的同学,会纠结于如何选择。其中有一些比较共性的问题,比如 “刚毕业,去做边缘业务,会不会影响后面的人生呢?”大家好,我是程序员鱼皮。今年的秋招已经接近尾声,我陆续收到了很多小伙伴们的 Offer 报喜。有一些同时拿到了多个 Offer 的同学,…