首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 24-03-12 16:27
  • 4359
  • 8328
blog.csdn.net

目录

概述

完美转发原理

完美转发失败的情形

实例讲解


概述

        std::forward是C++11引入的函数模板,它的作用是实现函数参数的完美转发,通俗的讲就是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发。

        std::forward原型:

  1. //左值版本
  2. template <class _Ty>
  3. _NODISCARD constexpr _Ty&& forward(
  4. remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
  5. return static_cast<_Ty&&>(_Arg);
  6. }
  7. //右值版本
  8. template <class _Ty>
  9. _NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
  10. static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
  11. return static_cast<_Ty&&>(_Arg);
  12. }

完美转发原理

        在C++中,存在左值(lvalue)和右值(rvalue)的概念。关于左值和右值的概念,自己可以查询相关资料。简单来说,左值是指可以取地址的、具有持久性的对象,而右值是指不能取地址的、临时生成的对象。传统上,当一个左值传递给一个函数时,参数会以左值引用的方式进行传递;当一个右值传递给一个函数时,参数会以右值引用的方式进行传递。

        以VS2019的std::forward源码实现来讲解:

1. 当传递给func函数的实参类型为左值QObject时,T被推导为QObject&类别。然后forward会实例化为std::forward,并返回QObject&(左值引用,根据定义是个左值!)

2. 而当传递给func函数的实参类型为右值QObject时,T被推导为QObject。然后forward被实例化为std::forward,并返回QObject&&(注意,匿名的右值引用是个右值!)

3. 可见,std::forward会根据传递给func函数实参(注意,不是形参)的左/右值类型进行转发。当传给func函数左值实参时,forward返回左值引用,并将该左值转发给doWork。而当传入func的实参为右值时,forward返回右值引用,并将该右值转发给doWork函数。

完美转发失败的情形

在std::forward中,失败的情况其实就是完美转发的目的没有达到,甚至根本无法实现

1、花括号的初始化方式调用

  1. #include
  2. #include
  3. void test(const std::array<int,5> ¶m){}
  4. template<typename T>
  5. void func(T&& t)
  6. {
  7. test(std::forward(t));
  8. }
  9. int main()
  10. {
  11. std::array<int, 5> arr = {0,1,2};
  12. func(arr);
  13. // func({0,1,2,3,4});
  14. }

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

2、0或者NULL做为空指针传递

其实这个也是c++11后强调使用nullptr做为指针的空值的一个重要原因。之所以会这样,是因为0和NULL往往会被默认转成为int类型,这也是在早期的C编译器中经常遇到的一个编译现象,就是“XXX无法转成int”其实就是写错了,但默认就是往int上靠,这个没办法。一如下面:

  1. void test( void *ptr){}
  2. template<typename T>
  3. void func(T&& t)
  4. {
  5. test(std::forward(t));
  6. }
  7. int main()
  8. {
  9. func(0);
  10. }

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

3、static const的应用(含constexpr)

  1. void test(int i){}
  2. class Data {
  3. public:
  4. static const int d_ = 1;
  5. };
  6. //const int Data::d_ ;
  7. template<typename T>
  8. void func(T&& t)
  9. {
  10. test(std::forward(t));
  11. }
  12. int main()
  13. {
  14. func(Data::d_);
  15. }

这段代码在VS中可以成功运行,但在g++中会报一个链接错误“ undefined reference to Data::d_ collect2: error: ld returned 1 exit status”。VS中对编译和链接相对来说还是宽松一些。
想通过连接只需要把注释的部分解开就可以了。
解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值

4、对重载和模板的函数名处理
这句话的意思是指在完美转发时如果参数是一个函数名称,那么如果这个函数名称的函数如果存在重载(或是模板)的话,在普通编程的直接调用情况下是没有问题,但是在完美转发时,不管是直接调用名称还是使用模板函数时会存在转发的错误。看下面的代码就理解了

  1. void myfunc_test(void func(int)) {}
  2. void myfunc(int a) {}
  3. void myfunc(int a, int b) {}
  4. template<typename T>
  5. void dotest(T t){}
  6. template<typename T>
  7. void func(T&& t)
  8. {
  9. myfunc_test(std::forward(t));
  10. }
  11. int main()
  12. {
  13. // func(myfunc);
  14. // func(dotest);
  15. myfunc_test(myfunc);
  16. }

一般来说,传递函数做为函数参数时,用函数指针的情况很多,但也应该知道,也可以直接传递函数名称而不是指针的情况来操作。上面的代码就是这样,就会出现本节的问题。但是如果直接调用函数myfunc_test而非完美转发,则没有问题。同样,函数模板也是如此,因为函数模板毕竟不是一个实例,而是一组类似实例的泛型。
想要解决这个问题也很简单,依照着普通编译成功的方式,采用指针的方式直接指明调用的函数即可,模板也同样如此。
5、位字段

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

解决方案:制作位域值的副本,并以该副本来调用转发函数。

  1. struct xxxxx
  2. {
  3. std::uint32_t v: 4,
  4. I : 4,
  5. D : 6,
  6. E : 2,
  7. t : 16;
  8. //...
  9. };
  10. void test(std::uint16_t v) {}
  11. template<typename T>
  12. void func(T&& t)
  13. {
  14. test(std::forward(t));
  15. }
  16. int main()
  17. {
  18. xxxxx ip = {};
  19. test(ip.t); //调用void func(int)
  20. //func(ip.t); //error,func形参是引用,由于位域是比特位组成。无法创建比特位的引用!
  21. //解决方案:创建位域的副本,并传给func
  22. auto length = static_castuint16_t>(ip.t);
  23. func(length);
  24. }

实例讲解

实例:

  1. template <typename _Callable, typename... _Types>
  2. auto invoke(_Callable&& obj, _Types... argv)
  3. {
  4. return _Invoke<_Callable, _Types...>::_Call(
  5. std::forward<_Callable>(obj),
  6. std::forward<_Types>(argv)...);
  7. }

目录

概述

完美转发原理

完美转发失败的情形

实例讲解


概述

        std::forward是C++11引入的函数模板,它的作用是实现函数参数的完美转发,通俗的讲就是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发。

        std::forward原型:

  1. //左值版本
  2. template <class _Ty>
  3. _NODISCARD constexpr _Ty&& forward(
  4. remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
  5. return static_cast<_Ty&&>(_Arg);
  6. }
  7. //右值版本
  8. template <class _Ty>
  9. _NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
  10. static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
  11. return static_cast<_Ty&&>(_Arg);
  12. }

完美转发原理

        在C++中,存在左值(lvalue)和右值(rvalue)的概念。关于左值和右值的概念,自己可以查询相关资料。简单来说,左值是指可以取地址的、具有持久性的对象,而右值是指不能取地址的、临时生成的对象。传统上,当一个左值传递给一个函数时,参数会以左值引用的方式进行传递;当一个右值传递给一个函数时,参数会以右值引用的方式进行传递。

        以VS2019的std::forward源码实现来讲解:

1. 当传递给func函数的实参类型为左值QObject时,T被推导为QObject&类别。然后forward会实例化为std::forward,并返回QObject&(左值引用,根据定义是个左值!)

2. 而当传递给func函数的实参类型为右值QObject时,T被推导为QObject。然后forward被实例化为std::forward,并返回QObject&&(注意,匿名的右值引用是个右值!)

3. 可见,std::forward会根据传递给func函数实参(注意,不是形参)的左/右值类型进行转发。当传给func函数左值实参时,forward返回左值引用,并将该左值转发给doWork。而当传入func的实参为右值时,forward返回右值引用,并将该右值转发给doWork函数。

完美转发失败的情形

在std::forward中,失败的情况其实就是完美转发的目的没有达到,甚至根本无法实现

1、花括号的初始化方式调用

  1. #include
  2. #include
  3. void test(const std::array<int,5> ¶m){}
  4. template<typename T>
  5. void func(T&& t)
  6. {
  7. test(std::forward(t));
  8. }
  9. int main()
  10. {
  11. std::array<int, 5> arr = {0,1,2};
  12. func(arr);
  13. // func({0,1,2,3,4});
  14. }

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

2、0或者NULL做为空指针传递

其实这个也是c++11后强调使用nullptr做为指针的空值的一个重要原因。之所以会这样,是因为0和NULL往往会被默认转成为int类型,这也是在早期的C编译器中经常遇到的一个编译现象,就是“XXX无法转成int”其实就是写错了,但默认就是往int上靠,这个没办法。一如下面:

  1. void test( void *ptr){}
  2. template<typename T>
  3. void func(T&& t)
  4. {
  5. test(std::forward(t));
  6. }
  7. int main()
  8. {
  9. func(0);
  10. }

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

3、static const的应用(含constexpr)

  1. void test(int i){}
  2. class Data {
  3. public:
  4. static const int d_ = 1;
  5. };
  6. //const int Data::d_ ;
  7. template<typename T>
  8. void func(T&& t)
  9. {
  10. test(std::forward(t));
  11. }
  12. int main()
  13. {
  14. func(Data::d_);
  15. }

这段代码在VS中可以成功运行,但在g++中会报一个链接错误“ undefined reference to Data::d_ collect2: error: ld returned 1 exit status”。VS中对编译和链接相对来说还是宽松一些。
想通过连接只需要把注释的部分解开就可以了。
解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值

4、对重载和模板的函数名处理
这句话的意思是指在完美转发时如果参数是一个函数名称,那么如果这个函数名称的函数如果存在重载(或是模板)的话,在普通编程的直接调用情况下是没有问题,但是在完美转发时,不管是直接调用名称还是使用模板函数时会存在转发的错误。看下面的代码就理解了

  1. void myfunc_test(void func(int)) {}
  2. void myfunc(int a) {}
  3. void myfunc(int a, int b) {}
  4. template<typename T>
  5. void dotest(T t){}
  6. template<typename T>
  7. void func(T&& t)
  8. {
  9. myfunc_test(std::forward(t));
  10. }
  11. int main()
  12. {
  13. // func(myfunc);
  14. // func(dotest);
  15. myfunc_test(myfunc);
  16. }

一般来说,传递函数做为函数参数时,用函数指针的情况很多,但也应该知道,也可以直接传递函数名称而不是指针的情况来操作。上面的代码就是这样,就会出现本节的问题。但是如果直接调用函数myfunc_test而非完美转发,则没有问题。同样,函数模板也是如此,因为函数模板毕竟不是一个实例,而是一组类似实例的泛型。
想要解决这个问题也很简单,依照着普通编译成功的方式,采用指针的方式直接指明调用的函数即可,模板也同样如此。
5、位字段

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

解决方案:制作位域值的副本,并以该副本来调用转发函数。

  1. struct xxxxx
  2. {
  3. std::uint32_t v: 4,
  4. I : 4,
  5. D : 6,
  6. E : 2,
  7. t : 16;
  8. //...
  9. };
  10. void test(std::uint16_t v) {}
  11. template<typename T>
  12. void func(T&& t)
  13. {
  14. test(std::forward(t));
  15. }
  16. int main()
  17. {
  18. xxxxx ip = {};
  19. test(ip.t); //调用void func(int)
  20. //func(ip.t); //error,func形参是引用,由于位域是比特位组成。无法创建比特位的引用!
  21. //解决方案:创建位域的副本,并传给func
  22. auto length = static_castuint16_t>(ip.t);
  23. func(length);
  24. }

实例讲解

实例:

  1. template <typename _Callable, typename... _Types>
  2. auto invoke(_Callable&& obj, _Types... argv)
  3. {
  4. return _Invoke<_Callable, _Types...>::_Call(
  5. std::forward<_Callable>(obj),
  6. std::forward<_Types>(argv)...);
  7. }

注:本文转载自blog.csdn.net的流星雨爱编程的文章"https://blog.csdn.net/haokan123456789/article/details/134980411"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top