大家好,我是 同学小张,持续学习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
来转发参数arg
到someFunction
。这样,无论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 一起交流💬,一起进步💪。
- 微信公众号也可搜【同学小张】 🙏
本站文章一览: