C++之std::forward完美转发的实现

 更新时间:2026年07月01日 08:59:09   作者:流星雨爱编程  
本文围绕C++的std::forward的实现,阐述了完美转发原理,即根据实参左/右值类型进行转发,还列举了完美转发失败的情形,如花括号初始化、空指针传递等,并给出相应解决方案

1.简介

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

std::forward原型:

//左值版本
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

//右值版本
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

2.为什么需要完美转发?

在C++中,函数参数的值的类别(左值或者右值)和类型信息在传递过程中可能会丢失,以下方函数为例:

#include<iostream>
void target(int& x) { std::cout << "lvalue\n"; }
void target(int&& x) { std::cout << "rvalue\n"; }

template<typename T>
void forwarder(T x) {
    target(x);  // 无论传入的是左值还是右值,x都是左值, 因为x是可以取地址的
}

int main() {
    int a = 10;
    forwarder(a);       // 期望调用 target(int&)
    forwarder(10);      // 期望调用 target(int&&)
    return 0;
 }

在上面代码中,forwarder函数无法区分传入的参数是左值还是右值,因为x在函数内部总是左值,想对这个进行区分,就需要引入完美转发。

3.完美转发原理

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

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

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

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

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

4.完美转发失败的情形

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

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

#include <iostream>
#include <array>

void test(const std::array<int,5> &param){}

template<typename T>
void func(T&& t)
{
    test(std::forward<T>(t));
}
int main()
{
    std::array<int, 5> arr = {0,1,2};
    func(arr);
   // func({0,1,2,3,4});
}

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

2.或者NULL做为空指针传递

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

void test( void *ptr){}

template<typename T>
void func(T&& t)
{
    test(std::forward<T>(t));
}
int main()
{
    func(0);
}

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

3.static const的应用(含constexpr)

void test(int i){}

class Data {
public:
    static const int d_ = 1;
};
//const  int Data::d_ ;

template<typename T>
void func(T&& t)
{
    test(std::forward<T>(t));
}
int main()
{
    func(Data::d_);
}

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

4.对重载和模板的函数名处理

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

void myfunc_test(void func(int)) {}

void myfunc(int a) {}
void myfunc(int a, int b) {}

template<typename T>
void dotest(T t){}
template<typename T>
void func(T&& t)
{
    myfunc_test(std::forward<T>(t));
}

int main()
{
   // func(myfunc);
   // func(dotest);
    myfunc_test(myfunc);
}

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

5.位字段

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

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

struct xxxxx
{
    std::uint32_t v: 4,
                  I : 4,
                  D : 6,
                  E : 2,
                  t : 16;
    //...
};

void test(std::uint16_t  v) {}

template<typename T>
void func(T&& t)
{
    test(std::forward<T>(t));
}
int main()
{
   xxxxx ip = {};
    test(ip.t);  //调用void func(int)
    //func(ip.t); //error,func形参是引用,由于位域是比特位组成。无法创建比特位的引用!

    //解决方案:创建位域的副本,并传给func
    auto length = static_cast<std::uint16_t>(ip.t);
    func(length);
}

5.实例讲解

实例:C++标准中的std::invoke

template <typename _Callable, typename... _Types>
auto invoke(_Callable&& obj, _Types... argv)
{
	return _Invoke<_Callable, _Types...>::_Call(
		std::forward<_Callable>(obj),
		std::forward<_Types>(argv)...);
}

到此这篇关于C++之std::forward完美转发的实现的文章就介绍到这了,更多相关C++ std::forward完美转发内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++中HTTP 代理服务器的设计与实现详解

    C++中HTTP 代理服务器的设计与实现详解

    代理服务器,即允许一个网络终端(一般为客户端)通过这个服务与另一 个网络终端(一般为服务器)进行非直接的连接,下面我们就来看看如何使用C++设计与实现一个HTTP 代理服务器吧
    2024-01-01
  • QT使用Http协议通信的实现示例

    QT使用Http协议通信的实现示例

    使用QT进行应用开发时,有时候需要进行客户端和服务端的网络通信,本文主要介绍了QT使用Http协议通信的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12
  • c语言根据用户输入的出生年份并计算出当前年龄

    c语言根据用户输入的出生年份并计算出当前年龄

    这篇文章主要介绍了c语言根据用户输入的出生年份并计算出当前年龄,需要的朋友可以参考下
    2023-03-03
  • vscode实现本地代码自动同步到远程机器的步骤

    vscode实现本地代码自动同步到远程机器的步骤

    这篇文章主要介绍了vscode实现本地代码自动同步到远程机器的步骤,本文分步骤给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • C/C++之动态内存管理方式(new delete)

    C/C++之动态内存管理方式(new delete)

    C++中new/delete集成内存分配与对象构造析构,是面向对象内存管理的核心,与malloc/free相比,支持类型安全、异常处理及数组操作,需严格配对使用,现代开发推荐智能指针替代,以提升安全性与自动化程度
    2025-09-09
  • C++实现经典24点纸牌益智游戏

    C++实现经典24点纸牌益智游戏

    这篇文章主要介绍了C++实现经典24点纸牌益智游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • C++进一步认识类与对象

    C++进一步认识类与对象

    类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例(Instance),拥有类的成员变量和成员函数
    2021-10-10
  • C++ std::condition_variable 条件变量用法解析

    C++ std::condition_variable 条件变量用法解析

    condition_variable(条件变量)是 C++11 中提供的一种多线程同步机制,它允许一个或多个线程等待另一个线程发出通知,以便能够有效地进行线程同步,这篇文章主要介绍了C++ std::condition_variable 条件变量用法,需要的朋友可以参考下
    2023-09-09
  • C语言实现读取CSV文件的方法详解

    C语言实现读取CSV文件的方法详解

    这篇文章主要为大家详细介绍了C语言如何实现读取CSV文件,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下
    2022-12-12
  • C语言矩阵连乘 (动态规划)详解

    C语言矩阵连乘 (动态规划)详解

    这篇文章主要介绍了C语言矩阵连乘 (动态规划)详解的相关资料,需要的朋友可以参考下
    2017-05-05

最新评论