【重学C++】【引用】循序渐进,理解现代c++中完美转发的概念及作用

大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。

重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。


上文(【重学C++】【引用】深入理解:右值引用(将亡值) 与 移动语义std::move)我们详细讨论了C++中的右值引用与移动语义std::move。本文我们继续探讨与右值引用相关的另一个C++特性 - 完美转发:std::forward

0. 完美转发的概念

0.1 什么是完美转发

C++的完美转发是一种特性,它允许模板函数准确地转发参数至另一个函数,而不会改变参数的值类别(lvalue 或 rvalue)和类型(包括引用属性)。这在编写泛型代码时非常有用,尤其是当你需要将参数无损地传递给其他函数时。

0.2 完美转发解决什么问题

  • 保持参数的值类别:在不使用完美转发的情况下,传递给模板函数的参数可能会丢失它们的rvalue引用属性,导致不能正确地转发给其他函数。

  • 保持参数的类型:完美转发确保了参数的类型在转发过程中不会发生改变,包括参数的const和volatile修饰符。

  • 提高代码的灵活性:它允许编写的代码更通用,能够适应不同的函数调用场景。

1. 完美转发的使用场景示例

假设有以下的代码:

template <typename T>
void wrapper(T&& arg) {
    // 这里arg可能是一个lvalue或rvalue,但是它被强制转换为了一个普通的引用
    // 如果arg是一个rvalue,那么下面的函数调用将不会正确处理它
    someFunction(arg);
}

void someFunction(const int& value) {
    // 处理value
}

int main() {
    int a = 10;
    int b = wrapper(a);  // a是lvalue,可以正常工作
    int c = wrapper(10); // 10是rvalue,但是被当作lvalue处理,可能不是预期的行为
}

分析一下以上的代码,wrapper是一个模板函数,接收一个右值引用,然后传递给somFunction

思考一下,wrapper的参数 arg 是右值引用,someFunction的参数 arg 还是右值引用吗?

答案是不是的,someFunction的参数 arg 变成了左值。为什么?

我们在上文(【重学C++】【引用】深入理解:右值引用(将亡值) 与 移动语义std::move)中讨论过:右值引用一定是右值吗?不是的,只要右值引用有名称,就是左值。

在这里插入图片描述
wrapper的参数 arg 虽然是右值引用,但是其有名称arg,所以其变成了左值,在传递给someFunction的时候,someFunction接收的就变成了左值,而不会保持右值引用。

这时候假设someFunction是一个构造函数,左值就会去调用复制操作,而如果保持右值才会去调用移动操作。移动操作由于这个原因就变成了复制,与我们想要的行为就会不符。这就是参数在传递过程中一些性质的隐式改变。

而为了保持参数在传递过程中保持属性和性质不变,C++提出了一个新的特性:完美转发,通过std::forward函数实现。

使用完美转发修改上面的程序:

template <typename T>
void wrapper(T&& arg) {
    // 使用std::forward完美转发arg,保持其值类别和类型
    someFunction(std::forward<T>(arg));
}

void someFunction(int&& value) {
    // 处理value,value保持了rvalue引用
}

int main() {
    int a = 10;
    int b = wrapper(a);  // a作为lvalue被转发
    int c = wrapper(10); // 10作为rvalue被转发,someFunction可以正确处理
}

我们使用了std::forward来转发参数argsomeFunction。这样,无论arg是一个lvalue还是rvalue,它都将保持其原始的值类别和类型。这意味着如果arg是一个rvalue,它将作为rvalue传递给someFunction,允许someFunction以期望的方式(例如移动语义的相关函数)处理它。

2. 循序渐进看完美转发的必要性

看到上面可能还是有点不直观,不知道为什么需要完美转发。别急,再通过一个例子,带大家循序渐进的看完美转发的由来。

下面的案例来自这篇文章:https://mp.weixin.qq.com/s?__biz=MzI4MTc0NDg2OQ==&mid=2247485130&idx=1&sn=a625763cbb67e0be02a47499cadbd5b5&chksm=eba5c240dcd24b561c8d652aeeccc6a5c03f629c16e3bd2f5cc046dce39e572736d248c0c986&cur_album_id=2857936376988811265&scene=189#wechat_redirect

假设我们有以下代码:

template<typename T, typename Arg>
std::shared_ptr<T> factory_v1(Arg arg)
{
    return std::shared_ptr<T>(new T(arg));
}

class X1 {
public:
    int* i_p;
    X1(int a) {
        i_p = new int(a);
        std::printf("call x1 constructor\n----------------------\n");
    }
};

下面两行代码的执行结果和过程是一模一样的。

auto x1_ptr_1 = factory_v1<X1>(5);
auto x1_ptr_2 = std::shared_ptr<X1>(new X1(5));

factory_v1 封装了 实例化过程,传递了 arg 参数,在传递的过程中arg保持原有的性质和属性,不会产生什么副作用,这就是完美转发

再来一个X2:

class X2 {
public:
    X2(){}
    X2(X2& rhs) {
        std::cout << "x2 copy constructor call" << std::endl;
    }
}

使用这个X2进行构造:

X2 x2 = X2();
auto x2_ptr_1 = factory_v1<X2>(x2);
std::printf("----------------------\n");
auto x2_ptr_2 = std::shared_ptr<X2>(new X2(x2));

运行结果如下:

在这里插入图片描述
factory_v1 多调用了一次拷贝构造函数,因为 factory_v1 接收的是值传递,在形参时会拷贝一份。

为了干掉这一个多余的形参拷贝,一般是将形参变成引用类型:

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v2(Arg& arg)
{
	return std::shared_ptr<T>(new T(arg));
}

但是这样的话,X1使用时就编译不过了:因为factory_v2需要传入一个左值,但字面量5是一个右值。

在这里插入图片描述
为了让两者都通用,修改这个工厂函数为:

const X& 类型的参数既能接收左值,又能接收右值。

template<typename T, typename Arg> 
std::shared_ptr<T> factory_v3(const Arg& arg)
{
	return std::shared_ptr<T>(new T(arg));
}

上面看似解决了问题,但如果再有一个类X3如下:

class X3 {
public:
    X3(){}
    X3(X3& rhs) {
        std::cout << "copy constructor call" << std::endl;
    }

    X3(X3&& rhs) {
        std::cout << "move constructor call" << std::endl;
    }
}

使用过程如下:

X3 x3 = X3();
std::printf("----------------------\n");
auto x3_ptr_1 = factory_v3<X3>(std::move(x3));
std::printf("----------------------\n");
auto x3_ptr_2 = std::shared_ptr<X3>(new X3(std::move(x3)));
std::printf("----------------------\n");

运行结果如下:

在这里插入图片描述
可以看到,factory_v3 最终调用的是拷贝,而不是移动。也就是说,在 factory_v3 的传递过程中,arg丢失了其原有的右值属性,变成了左值。这就不是完美转发了。

加上完美转发:

template<typename T, typename Arg>
std::shared_ptr<T> factory_v4(Arg&& arg)
{
    return std::shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

运行结果:保持了右值属性

auto x3_ptr_3 = factory_v4<X3>(std::move(x3));

// 输出
// x3 move constructor call

这样,我们就实现了完美转发。

值得注意的是,factory_v4的形参类型为 Arg&&,而不是 const Arg&。记住这一点就好,必须是 Arg&& 类型接收参数。这样做的原因涉及到C++中的引用折叠规则,暂时不作详细解释。

3. 总结

本文我们主要介绍了完美转发的概念和用途。实现完美转发总结起来就两步:

(1)在模板中使用&&接收参数。

(2)使用std::forward()转发给被调函数。

这样左值作为仍旧作为左值传递,右值仍旧作为右值传递!

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~


  • 大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例
  • 欢迎 点赞 + 关注 👏,持续学习持续干货输出
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜同学小张 🙏

本站文章一览:

在这里插入图片描述

  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

同学小张

如果觉得有帮助,欢迎给我鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值