C++展开模板参数包、函数参数包-(lambda+折叠表达式)
创始人
2024-05-25 10:54:44
0

开门见山

以下代码可展开模板参数包和展开函数参数包。

// lambda+折叠表达式(需C++17)
#include 
using namespace std;// 1.展开模板参数包
template
void Func1()
{([]() {cout << typeid(T).name() << endl;}(), ...);// 折叠表达式
}
// 2.展开函数参数包
template
void Func2(T... args)
{([&args]() {cout << args << endl;}(), ...);// 折叠表达式
}
void main(){cout << "开门见山" << endl;cout << "1.展开模板参数包" << endl;Func1();cout << endl;cout << "2.展开函数参数包" << endl;Func2('c', true, 2, 'h', "CSDN越来越好。。。");
}

请添加图片描述

文章目录

  • 开门见山
  • 前言
    • 提前声明
    • 遇到的问题
  • 一步一步理解过程
    • 1.1 模板推断类型
    • 1.2 参数包转发、递归解函数包
    • 1.3 不用递归解包-外部函数-折叠表达式
    • 1.4 不用递归解包-lambda函数-折叠表达式
    • 1.5 lambda+折叠表达式解开模板参数包
    • 1.6 回归组件代码

前言

提前声明

  • 关于

    我菜鸟一枚,文中的术语、代码、文字若有错误,欢迎指正

  • 阅读本文需要了解的知识

    模板、模板参数包、函数参数包、参数包转发、扩展参数包、折叠表达式、lambda函数。

  • 本文目的

    是记录自己遇到的C++语法问题如何运用所学知识慢慢理解的过程,不属于学习语法文章。

    相关不理解知识点建议百度。

遇到的问题

  • 需求说明

    • 在Unity的游戏引擎中,Ctrl+D复制一个物体,新物体应该具有旧物体的各个组件。

    • 组件列表有:TransformComponent、SpriteRendererComponent、CircleRendererComponent。

    • 如果要复制这三个组件给新物体,假设Unity内部实现代码如下

    template
    static void CopyComponentIfExists(Entity dst, Entity src) {if (src.HasComponent()) {// 新物体添加旧物体组件dst.AddOrReplaceComponent(src.GetComponent());}
    }
    void Scene::DuplicateEntity(Entity entity)
    {// 1.创建旧实体同名的新实体std::string name = entity.GetName();Entity newEntity = CreateEntity(name);// 2.复制组件CopyComponentIfExists(newEntity, entity);CopyComponentIfExists(newEntity, entity);CopyComponentIfExists(newEntity, entity);
    }
  • 简化代码

    若组件列表有20个,那么CopyComponentIfExists这行代码需要20个,显然代码会冗余,应使用模板来简化此代码,代码如下

    template
    struct ComponentGroup {
    };
    using AllComponents = ComponentGroup;template
    static void CopyComponentIfExists(Entity dst, Entity src) {([&]() {if (src.HasComponent()) {// 新物体添加旧物体组件dst.AddOrReplaceComponent(src.GetComponent());}}(), ...);
    }    
    template
    static void CopyComponentIfExists(ComponentGroup, Entity dst, Entity src) {CopyComponentIfExists(dst, src);
    }
    void Scene::DuplicateEntity(Entity entity)
    {// 1.创建旧实体同名的新实体std::string name = entity.GetName();Entity newEntity = CreateEntity(name);// 2.复制组件CopyComponentIfExists(AllComponents{}, newEntity, entity);//CopyComponentIfExists(newEntity, entity);//CopyComponentIfExists(newEntity, entity);//CopyComponentIfExists(newEntity, entity);
    }
    
    • 模板代码完成新物体拷贝旧物体组件的任务。

    • 假设有20个组件类型,只要在AllComponents写上这20个组件,再调用CopyComponentIfExists(AllComponents{}, newEntity, entity);就不用像之前写20行代码。

    • 但是缺点也产生了,就是代码变得很难理解了。

  • 简化核心代码( tips:这里被下面过程称为 组件代码

    我看到上一小点的代码,不知模板如何运作的,于是写下以下简洁版代码,只要理解了以下代码等同于理解上一小点的代码

    #include 
    using namespace std;struct TransformComponent {TransformComponent() { cout << "TransformComponent()" << endl; }
    };
    struct SpriteRendererComponent {SpriteRendererComponent() { cout << "SpriteRendererComponent()" << endl; }
    };
    struct CircleRendererComponent {CircleRendererComponent() { cout << "CircleRendererComponent()" << endl; }
    };template
    struct ComponentGroup {ComponentGroup() { cout << "ComponentGroup()" << endl; }
    };
    using AllComponents = ComponentGroup;// 为复制实体的辅助方法
    template
    static void CopyComponentIfExists() {([]() {cout  << typeid(Component).name() << endl;}(), ...);
    }
    template
    static void CopyComponentIfExists(ComponentGroup) {CopyComponentIfExists();
    }
    void main() {CopyComponentIfExists(AllComponents{});
    }
    

    这简化核心版的代码(别称:组件代码)最重要的就是理解:如何展开模板参数包

    由于在网上搜索相关模板参数知识点,都是讲如何展开函数参数包的,没有讲如何展开模板参数包(也许我自己菜,没有搜到),所以无奈,在自己花了一下午的时间,自己慢慢试出来了,编码理解过程如下。

一步一步理解过程

1.1 模板推断类型

  • 参考例子1

    #include 
    #include 
    using namespace std;template
    static void Func1(T t) {T t2 = 8;cout << t << " " << t2 << endl;
    }
    template
    static void Func2() {T t = 7;cout << t << endl;
    }
    void main() {Func1(3);Func1(4);// 这就是省略了声明模板类型,由参数4推断Func1的模板T类型为intFunc2();
    }
    
  • 所以可以理解以下代码

    template
    struct ComponentGroup {ComponentGroup() { cout << "ComponentGroup()" << endl; }
    };
    using AllComponents = ComponentGroup;template
    static void CopyComponentIfExists(ComponentGroup) {// 传给CopyComponentIfExists的模板参数包,为当前函数参数包推断出来的模板参数包CopyComponentIfExists();
    }
    void main() {// 传入AllComponents{}代表传入实参,CopyComponentIfExists的模板类型从实参推断出来CopyComponentIfExists(AllComponents{});
    }
    

    在CopyComponentIfExists函数内

    1. ComponentGroup<>,是带有模板参数包的struct

    2. ComponentGroup

      Component…模板参数包由函数参数包AllComponents{}推断出来,像上面例子1的Func1(4);

      由于AllComponents = ComponentGroup

      传入的函数参数包AllComponents{}

      则能推断出模板参数包
      Component… = TransformComponent, SpriteRendererComponent,CircleRendererComponent

    3. 得到了Component…模板参数包后,再当做显示的模板传给下个函数

      代码CopyComponentIfExists();
      相当于
      CopyComponentIfExists();

1.2 参数包转发、递归解函数包

  • 此小节为理解

    • 参数包转发
    • 解函数包概念
  • 则给出以下代码

    例子2:显示指定模板参数包类型、并传入函数参数包(与上有点不同,为理解解包概念)、并递归解函数包

    #include 
    using namespace std;// 2.递归解函数包
    void Func2() { cout << "Func2(),因为解包完了,无参数,就调用此函数,代表递归解包结束" << endl; }// 递归终止函数template
    void Func2(T& val, TT... args)                          // 函数参数包的第一个赋给第一个参数,剩下的都给参数包args
    {cout << val << "-->" << typeid(val).name() << endl;// 打印获取当前参数包的第一个参数值和类型// 继续解包,将函数参数包传给本函数递归,不指定模板参数包类型,由函数参数包推断出来Func2(args...);                                     
    }
    // 1.参数包转发
    template
    void Func1(T... args) {/*传给Func2函数的模板参数包,为当前函数参数包推断出来的模板参数包,并将多个实参传入与CopyComponentIfExists();不同,这里有传递实参*///Func2(args...); // 转发模板参数包,函数参数包Func2(args...);// 与上一段代码调用一样,只不过显示指定模板参数包类型
    }
    void main() {cout << "参数包转发、递归解包"<< endl;Func1(2, 3, 'c', "12312");
    }
    

    请添加图片描述

1.3 不用递归解包-外部函数-折叠表达式

  • 引入

    由1.2的例子解包,知道了解包概念和流程,但是为靠近一开始的组件代码,不使用递归而实现的解包代码如下

    #include 
    using namespace std;// 2.不用递归解包
    template 
    void Func2(T val)
    {cout << val << "-->" << typeid(val).name() << endl;
    }
    template
    void Func2(T... args)                          
    {/* 重点在这:(func(args), ...);意思是逐个展开args函数参数包,并将解开的一个参数传入Func2,有多少个参数就有多少个Func2的调用。可以理解展开的语句为:Func2(2), Func2(3), Func2('c'), Func2("12312");*/(Func2(args), ...);// 折叠表达式解包
    }
    // 1.参数包转发
    template
    void Func1(T... args) {/*传给Func2函数的模板参数包,为当前函数参数包推断出来的模板参数包,并将多个实参传入与CopyComponentIfExists();不同,这里有传递函数参数包*/Func2(args...); //Func2(args...);// 与上一段代码调用一样
    }
    void main() {cout << "不用递归解包-外部函数"<< endl;// 相当于Func2(2, 3, 'c', "12312");但为了与前一致讲述参数包转发,所以还是Func1Func1(2, 3, 'c', "12312"); 
    }
    

    请添加图片描述

1.4 不用递归解包-lambda函数-折叠表达式

由1.3例子的重点那段注释,Func2(args)可以改写成lambda匿名函数

#include 
using namespace std;// 2.不用递归解包-并用lamda
template
void Func2(T... args)
{/*重点在这:([&](){}(), ...);意思是逐个展开args函数参数包,并将解开的一个参数被lambda捕获,有多少个参数就有多少个lambda的调用。可以理解展开的语句为:args = 2;       [&]() {cout << args << "-->" << typeid(args).name() << endl; }(); args = 3;       [&]() {cout << args << "-->" << typeid(args).name() << endl; }(); args = 'c';     [&]() {cout << args << "-->" << typeid(args).name() << endl; }(); args = "12312"; [&]() {cout << args << "-->" << typeid(args).name() << endl; }();实际上args函数参数包被展开的新变量名为:args_0、args_1、args_2、args_3*/// & 隐式引用捕获的是解开args函数参数包,得到一个参数赋给args的变量([&]() {cout << args << "-->" << typeid(args).name() << endl;}(), ...);// 折叠表达式解包
}
// 1.参数包转发
template
void Func1(T... args) {Func2(args...);
}
void main() {cout << "不用递归解包-lambda" << endl;Func1(2, 3, 'c', "12312");
}

请添加图片描述

1.5 lambda+折叠表达式解开模板参数包

由1.4的例子发现与原组件代码很接近了,但是原组件代码并没有函数参数包传递,只有推断出来的模板参数包传递。

于是问题再于是否能像1.4代码用lamda解函数参数包那样解开模板参数包呢?答案是肯定的,如下

#include 
using namespace std;// 2.用lamda解开模板参数包
template
void Func2()
{// & 隐式引用捕获的是解开模型参数包,得到一个类型赋给T的变量// 但是不写&也行,1.4节要写也许args是参数在函数体内,而这里T是类型且在函数体外所以不用?([]() {cout << typeid(T).name() << endl;}(), ...);// 折叠表达式
}
// 1.模板参数包转发
template
void Func1(T... args) {// 模板参数包的由参数包推断出来//  = ,转发模板参数包Func2();
}
void main() {cout << "类似组件(代码)-模板参数包转发-lambda解包" << endl;Func1(2, 3, 'c', "12312");
}

请添加图片描述

1.6 回归组件代码

由以上的步骤,不难理解原本的组件代码意思和流程了

  1. 传入AllComponents{}代表传入实参
  2. 第一个函数的模板参数包由函数参数包推断出来
  3. 第一个函数推断出来的模板参数包转发,传递给第二个函数
  4. 第二个函数接收到传过来的模板参数包
  5. 折叠表达式解开模板参数包,并用lambda输出
#include 
using namespace std;struct TransformComponent {TransformComponent() { cout << "TransformComponent()" << endl; }
};
struct SpriteRendererComponent {SpriteRendererComponent() { cout << "SpriteRendererComponent()" << endl; }
};
struct CircleRendererComponent {CircleRendererComponent() { cout << "CircleRendererComponent()" << endl; }
};template
struct ComponentGroup {ComponentGroup() { cout << "ComponentGroup()" << endl; }
};
using AllComponents = ComponentGroup;// 4.接收到传过来的模板参数包
//  = 
template
static void CopyComponentIfExists() {// 5.解开模板参数包,并用lambda输出([]() {cout  << typeid(Component).name() << endl;}(), ...);// (, ...)折叠表达式解包
}
// 2.模板参数包由函数参数包推断出来
template
static void CopyComponentIfExists(ComponentGroup) {// 3.推断出来的模板参数包转发,传递CopyComponentIfExists();//  = 
}
void main() {cout << "1.6 回归组件代码"<});// 1.传入AllComponents{}代表传入实参
/*
由于:AllComponents{} = ComponentGroup()
所以:CopyComponentIfExists(AllComponents{});等价CopyComponentIfExists(ComponentGroup());
*/
}

请添加图片描述

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...