std::forward与完美转发详解

  • 概述

    std::forward是C++11中引入的一个函数模板,用于实现完美转发(Perfect Forwarding)。它的作用是根据传入的参数,决定将参数以左值引用还是右值引用的方式进行转发。

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

    然而,完美转发是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题。在C++03中,可以使用泛型引用来实现完美转发,但是需要写很多重载函数,非常繁琐。而在C++11中,引入了std::forward,可以更简洁地实现完美转发。

    因此,概括来说,std::forward实现完美转发主要用于以下场景:提高模板函数参数传递过程的转发效率

    下面我们将逐步引入完美转发的必要性和用法。完美转发主要通过“引用折叠”和“std::forward”函数实现,我们先来了解他们。

  • 引用折叠

C++引用折叠是一种特性,允许在模板元编程中使用引用类型的参数来创建新的引用类型。

由于存在T&&这种万能引用类型,当它作为形参时,有可能被一个左值引用或右值引用的参数初始化,这是经过类型推导的T&&类型,推导后得到的参数类型会发生类型变化,这种变化就称为引用折叠。

引用折叠的具体规则如下:

  • 若一个右值引用(即带有&&)参数被一个左值或左值引用初始化,那么引用将折叠为左值引用。(即:T&& & –> T&)
  • 若一个右值引用参数被一个右值初始化,那么引用将折叠为右值引用。(即:T&& && 变成 T&&)。
  • 若一个左值引用参数被一个左值或右值初始化,那么引用不能折叠,仍为左值引用(即:T& & –>T&,T& && –>T&)。
总结一下: 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&) 。 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)。
简单来说: 右值经过T&&参数传递,类型保持不变还是右值(引用);而左值经过T&&变为普通的左值引用。

为了更好地理解引用折叠,以下为几个示例:

template<typename T>
void func(T&& arg);

int main() {
  int a = 5;
  func(a);  // arg为int&,引用折叠为左值引用

  func(10);  // arg为int&&,引用折叠为右值引用

  int& ref = a;
  func(ref);  // arg为int&,引用不能折叠
}

在上述示例中,函数模板func接受一个转发引用类型(&&)的参数,并根据传递给func的实参类型决定引用类型:

  • 当func(a)时,参数类型折叠后实际为int&,因为a是一个左值,引用类型折叠为左值引用。
  • 当func(10)时,参数类型折叠后实际为int&&,因为10是一个右值,引用类型折叠为右值引用。
  • 当func(ref)时,参数类型折叠后实际为int&,由于左值引用类型不能折叠,参数类型保持为左值引用。

引用折叠是C++中模板编程中非常有用的特性,可以根据传递实参的左值还是右值来确定引用类型,进而使得编写通用的模板函数或类更简单。

std::forward函数

实现完美转发的关键是使用std::forward函数。std::forward是一个条件转发函数模板,根据参数的左值或右值属性进行转发。它的定义参考如下:

这个模板函数接受一个参数并返回一个右值引用,同时利用引用折叠保留参数的左值或右值属性。调用std::forward时,根据参数的左值或右值属性,编译器会选择适当的模板实例进行转发。如果参数是一个左值引用,std::forward将返回一个左值引用。如果参数是一个右值引用,std::forward将返回一个右值引用。

例如:

1、如果T为std::string&,那么std::forward(t) 返回值为std::string&& &,折叠为std::string&,左值引用特性不变。

2、如果T为std::string&&,那么std::forward(t) 返回值为std::string&& &&,折叠为std::string&&,右值引用特性不变。

掌握了以上知识之后,我们可能还是不清楚std::forward到底有什么用,那么请看下一节。

利用std::forward实现完美转发

C++完美转发是指一种能够传递函数参数或对象的同样类型(例如左值或右值属性)和cv限定符(const或volatile)的方式,同时保留原参数的准确数值类别和cv限定符的转发机制。完美转发通过使用引用折叠机制和std::forward函数来实现。

作用

在C++11之前,当我们将一个参数转发给另一个函数时,会丢失参数的左值或右值的信息。例如,如果我们有一个函数f,它接受一个左值引用,然后我们通过f来调用一个函数g并传递一个右值,那么在g函数内部,参数将被视为左值,从而可能引入额外的参数转移开销。

C++11引入了右值引用、移动构造函数、引用折叠、std::forward等概念,使我们能够更准确地传递参数的左值或右值属性。因此,完美转发的目标是在转发参数时保持原始参数的左值或右值属性,从而提高函数参数传递的效率。

完美转发应用实例

首先定义一个对象CData,具体说明看注释:

#include <stdio.h>
#include <unistd.h>
#include <iostream>

class CData
{
public:
	CData() = delete;
	CData(const char* ch) : data(ch)    // 构造函数,涉及资源的复制
	{
		std::cout << "CData(const char* ch)" << std::endl;
	}
	CData(const std::string& str) : data(str)  // 拷贝构造函数,涉及资源的复制
	{
		std::cout << "CData(const std::string& str)" << std::endl;
	}
	CData(std::string&& str) : data(str)    // 移动构造函数,不涉及资源的复制!!!
	{
		std::cout << "CData(std::string&& str)" << std::endl;
	}
	~CData()   // 析构函数
	{
		std::cout << "~CData()" << std::endl;
	}
private:
	std::string data;   // 表示类内部管理的资源
};

注:

假如我们封装了一个操作,主要是用来创建对象使用(类似设计模式中的工厂模式),这个操作要求如下:

1. 可以接受不同类型的参数,然后构造一个对象的指针。

2. 性能尽可能高。(这里需要高效率,故对于右值的调用应该使用CData(std::string&& str)移动函数操作)

1)不使用std::forward实现

假设我们不使用std::forward,那么要提高函数参数转发效率,我们使用右值引用(万能引用)作为模板函数参数:

template<typename T>
CData* Creator(T&& t) { // 利用&&万能引用,引用折叠: T&& && -> T&&; T&& & -> T&
	return new CData(t);
}
int main(void) {
    std::string str1 = "hello";  
    std::string str2 = " world";
 // 参数折叠为左值引用,调用CData构造函数
    CData* p1 = Creator(str1);      
// 参数折叠为右值引用,但在Creator函数中t仍为左值,调用CData拷贝构造函数!!!
    CData* p2 = Creator(str1 + str2);
    delete p2;
    delete p1;

    return 0;
}

g++编译上述程序,可得如下结果,印证了注释中的说明:

可以看出,在不使用std::forward的情况下,即使传入了右值引用,也无法在Creator函数中触发CData的移动构造函数,从而造成了额外的资源复制损耗。

注:当右值被一个名字(即变量名、引用、指针等)接收时,这个名字其实是一个左值,名字所绑定的资源是一个右值。所以函数CData* Creator(T&& t) {return new CData(t); }内的t一定是左值引用。

2)使用std::forward实现

使用std::forward即可完美解决上述问题:

template<typename T>
CData* Creator(T&& t) {
    return new CData(std::forward<T>(t));
}
int main(void) {
    std::string str1 = "hello";
    std::string str2 = " world";
    CData* p1 = Creator(str1);        // 参数折叠为左值引用,调用CData构造函数
    CData* p2 = Creator(str1 + str2); // 参数折叠为右值引用,通过std::forward转发给CData,调用移动构造函数
    delete p2;
    delete p1;

    return 0;
}

g++编译上述程序,可得如下结果,印证了注释中的说明:

可以看出,使用了std::forward之后,可以将传入的函数参数按照其原类型进一步传入参数中,从而使右值引用的参数类型可以触发类的移动构造函数,从而避免不必要的资源复制操作,提高参数转移效率。

结论

所谓的完美转发,是指std::forward会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。

完美转发主要使用两步来完成任务: 1. 在模板中使用&&(万能引用)接收参数。 2. 使用std::forward()转发给被调函数.

这个对于上面一个例子带来的好处就是函数转发仍旧为右值引用,可以使用移动构造函数提高参数转移的效率(关于移动构造函数可以参考上一篇文章《C++编程系列笔记(2)——std::move和移动语义详解》中的内容)。

std::forward实现原理(参考自ChatGPT)

std::forward的定义如下:

template <typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept
{   // forward an lvalue as either an lvalue or an rvalue
    return (static_cast<_Ty&&>(_Arg));
}

它接受一个参数arg,并将其转发为右值引用。实际上,它会根据arg的左值或右值属性来决定是将arg转发为左值引用还是右值引用。如果arg是左值,它会将arg转发为左值引用;如果arg是右值,它会将arg转发为右值引用。

std::forward通常与模板函数和转发引用(forwarding reference)一起使用,用于完美转发函数参数。例如:

template <typename T>
void foo(T&& arg)
{
    bar(std::forward<T>(arg));
}

在这个例子中,foo函数接受一个转发引用arg,并将arg转发给另一个函数bar。通过使用std::forward,bar函数会根据传入的arg参数的左值或右值属性,将它作为左值引用或右值引用进行转发。

总之,std::forward是C++11中用于实现完美转发的函数模板,可以根据传入的参数决定将参数以左值引用还是右值引用的方式进行转发,解决了传递参数时临时对象被强制转换为左值的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值