首页 > 其他分享 >16、std::forward与完美转发详解

16、std::forward与完美转发详解

时间:2024-01-25 14:35:38浏览次数:27  
标签:std 右值 16 左值 引用 && forward

概述

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

  然而,完美转发是为了解决传递参数时的临时对象(右值)被强制转换为左值的问题。在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(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* p1 = Creator(str1);       // 参数折叠为左值引用,调用CData构造函数
    CData* p2 = Creator(str1 + str2);// 参数折叠为右值引用,但在Creator函数中t仍为左值,调用CData构造函数!!!
    delete p2;
    delete p1;

    return 0;
}

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

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

 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,右值,16,左值,引用,&&,forward
From: https://www.cnblogs.com/zwj-199306231519/p/17987070

相关文章

  • CodeForces 1667E Centroid Probabilities
    洛谷传送门CF传送门首先需要了解重心的三种定义:删掉一个点后剩下子树大小\(\le\frac{n}{2}\)的点\(\sum\limits_{i=1}^n\text{dis}(u,i)\)最小的点最深的\(sz_u\ge\left\lceil\frac{n}{2}\right\rceil\)的点这道题我们使用第三种定义,也就是要统计\(i\)为最......
  • angular 16 路由守卫更新
    在angular16中守卫使用方式进行了更新,route守卫被弃用(取消了CanActivate的使用),新增了功能性守卫(CanActivateFn),支持inject注入,官网提供了一个新的类型exportdeclaretypeCanDeactivateFn<T>=(component:T,currentRoute:ActivatedRouteSnapshot,currentState:Route......
  • Ybt 金牌导航 6.1.H. 时空旅行 / P5416 [CTSC2016] 时空旅行(线段树分治+凸包)
    题意简述初始有版本\(0\),其中仅包含点\(0\),且\(c_0\)给出,\(x_0=0\)。对于第\(i\)个版本,它依赖第\(fr_i\)个版本,而且会在父级版本的基础上进行以下两种操作之一:插入一个新点,并且会给出\(x_i\)和\(c_i\)。删除一个本就存在的点(不为\(0\))给出\(m\)次询问,每次给出......
  • 寒假生活指导16
    importrequestsurl='http://www.baidu.com'response=requests.get(url=url)#一个类型和六个属性#Response类型print(type(response))#设置响应的编码格式response.encoding='utf-8'#以字符串的形式来返回了网页的源码print(response.text)#返回一......
  • Ybt 金牌导航 6.3.A. 区间众数 / P4168 [Violet] 蒲公英(分块)
    题意简述多次查询区间\([l,r]\)的众数,若有多个取数值最小的。强制在线。\(n\le4\times10^4,m\le5\times10^4\)。分析考虑分块。首先预处理出块区间内的众数\(maj_{l,r}\)和每种数在某个块的前缀的出现次数\(cnt_{i,a_i}\),时空复杂度都是\(O(n\sqrtn)\)的。对于询......
  • std::function类的使用示例
    std::function是C++标准库中的一个模板类,用于封装可调用的目标,比如函数、函数指针、成员函数指针、Lambda表达式等,使得它们可以像普通函数一样被调用。这种灵活性使得std::function在许多场景下都非常有用。以下是std::function的一般用法:1.封装函数指针1.1不带参数和返回值......
  • day25 代码随想录算法训练营 216. 组合总和 III
    题目:216.组合总和III我的感悟:还是按照之前的套路来。多了一个参数path_sum应该是有两处剪枝,1处横线剪枝,1处纵向剪枝?或者说1处求和剪枝?1处范围剪枝?【疑问】理解难点:不剪枝的已经模的差不多了,剪枝的再看看 自己听了一遍写的:[未剪枝]classSolution:defcombina......
  • CF1689A题解
    题意简述给定字符串\(a\)和\(b\),每次从\(a\)串或\(b\)串中选出一个字符加入\(c\)串,要求\(c\)串的字典序最小。特别地,在\(c\)串中不能出现连续\(k\)次来源相同的字符。思维路径由于字符是随意选取的,易于发现每次选\(a\)串中字典序最小的字符或者\(b\)串中字......
  • 163邮箱下载方法
    在现代社会中,电子邮件已经成为人们日常生活和工作中不可或缺的一部分。163邮箱作为国内知名的电子邮件服务提供商,为用户提供了方便快捷的邮件通讯方式。为了更好地利用163邮箱,以下是一些简便的下载方法,让您随时随地轻松管理您的电子邮件。1.登录163邮箱账户首先,要想进行邮箱中的信......
  • STM32CubeMX教程23 FSMC - IS62WV51216(SRAM)驱动
    1、准备材料开发板(正点原子stm32f407探索者开发板V2.4)STM32CubeMX软件(Version6.10.0)野火DAP仿真器keilµVision5IDE(MDK-Arm)ST-LINK/V2驱动XCOMV2.6串口助手2、实验目标使用STM32CubeMX软件配置STM32F407开发板的FSMC实现以轮询或DMA的方式读写IS62WV51216(SRAM)芯片3、......