大家好,我是 同学小张,持续学习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() 转发给被调函数。 这样左值作为仍旧作为左值传递,右值仍旧作为右值传递! 如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~点击上方公众号,关注↑↑↑
· 大家好,我是 同学小张,日常分享AI知识和实战案例 · 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。 · +v: jasper_8017 一起交流💬,一起进步💪。
公众号内文章一览
|