variadic templates(可变参数模板)各版本异同

学习内容

本节学习可变参数模板各版本异同,后续请关注 学习C++11/14/17/20/23关键词版本更替 ,将持续更新~~

前情提要

  • Pack(包),C++定义的一类实体,代表一组可扩展的元素

    • 模板参数包:在 C++11 之前,函数模板或类模板的参数数量是固定的。例如,template<typename T, typename U> 只能接受两个类型参数。而有了模板参数包,可以写成 template<typename... Args>,其中的 Args 就是一个模板参数包,它可以代表 0 个、1 个或多个类型参数

      cpp 复制代码
      template <typename... Ts>
      struct MyTuple{};
      
      MyTuple<> t1;
      MyTuple<int> t2;
      MyTuple<int ,float ,char> t3;
      MyTuple<0>  t4; // error, 只允许带入类型,不会隐式推导
      
      template<typename U , typename... Ts>  //遵守模板参数包在模板参数列表最后一个参数
      struct Valid;
      
      template<typename... Ts, typename U , typename = void>
      void Valid(U u ,Ts... ts);  //ok , 因为可扩展包在参数列表最后一个参数
      
      Valid( 1.f , 1 , 2 ,3 );  // U类型为double , Ts类型为 { int ,int ,int }
      
      // 错误示例
      template<typename... Ts, typename U>
      void add(Ts... arg , U u )    // 参数包必须写在函数模板的最后一个参数位置 ,否则在调用时编译器无法确定有效参数个数
      {
          (std::cout << ... << arg) << std::endl;
      }
      
      // 错误示例
      template<typename... Ts,typename U>  // 当扩展包出现在类模板中时,必须作为最后一个模板参数出现 
      struct Exam {};
    • 函数参数包,函数中可以接受零个或多个函数实参的参数

      cpp 复制代码
      template< typename... Args>
      void print(Args... args) { (std::cout << ... << args ) << std::endl; }
      
      print();
      print(1);
      print(2.f,100);
    • lambda初始化捕获包(C++20),通过包展开来捕获多个初始化器

      在C++20之前 ,参数包(如 Args... args)并不是一个集合,lambda 的捕获列表只能捕获单个变量。直接写 [args...] 或 [&args...] 语法可以通过,但每个元素都是独立捕获的,无法对整个包进行复制、移动或类型转换。 C++20时进行改进,允许将包打包成一个对象 ,[capture = expression],把参数包展开并存入 std::tuple、std::array 或自定义结构中

      cpp 复制代码
      template<typename ... Args>
      void foo(Args... args)
      {
          auto lamdba = [args...]{ };// 使用args... 的副本
          auto lamdbaRef = [&args...]{ };// 引用args... 
      }
      
      template<typename ... Args>
      void foos(Args... args)
      {
      	// 将扩展包作为一个独立的整体进行move,并构造一个tuple
          auto lambda = [tup = std::make_tuple(std::move(args)...)]
          {
              std::apply([](auto&... elements)
              {
                      (std::cout << ... << elements) << std::endl;
              },tup);
          };
      }
    • 结构化绑定包(C++26)

      结构化绑定声明中,用于引入零个或多个绑定标识符 ,通过使用...rest的语法,将其中一部分元素打包成一个新的参数包

      cpp 复制代码
      int arr[4] = { 1 ,2, 3, 4 };
      auto [...arrs] = arr;  //结构化绑定
      
      std::tuple<int, double, std::string> data{42, 3.14, "hello"};
      // 捕获所有元素为一个包
      auto [...all] = data;
      // all 是一个参数包,包含元素 {42, 3.14, "hello"}
      
      // 捕获第一个元素,其他元素组成包
      auto [first, ...rest] = data;
      // first 是 int(42)
      // rest 是一个参数包,包含元素 {3.14, "hello"}
      
      //  捕获最后一个元素,前面元素组成包
      auto [...init, last] = data;
      // last 是 string("hello")
      // init 是一个参数包,包含元素 {42, 3.14}
      
      // 混合使用:捕获头部和尾部元素,中间打包
      auto [fst, ...mid, lst] = data;
  • 模板参数包

    • 常量模板参数包

      cpp 复制代码
      template <int ... Values>
      struct A { static constexpr int value = (0 + ... + Values); };
    • 类型模板参数包

      cpp 复制代码
      template<typename... Types> 
      struct B{};
    • 受约束的类型模板参数包

      cpp 复制代码
      template <std::integral... Ts>
      void f(Ts... args){}
    • 模板模板参数包

      cpp 复制代码
      template <template<typename>class ... Containers>
      struct ContainerList{};

Pack展开,一个模式后面跟着省略号...,并且该模式至少出现一个包的名字

  • 单包展开
cpp 复制代码
template<class... Us>
void f(Us... us) {}

template<class... Ts>
void g(Ts... ts) {  f(&args...);  }  // 其中,&args... 是一个包展开,&args称为其模式

g(1 , 0.2 , "a"); // Ts...展开为(int ,double ,const char*)

args...展开为三个实参 ( p1,p2,p3 ), &args...展开为 (&p1 , &p2 , &p3) ,最终调用为 f(int* p1, double* p2 , const char** p3)

  • 多包展开
cpp 复制代码
template <typename...>
	struct Tuple{};

	template <typename T1 , typename T2>
	struct Pair{};	
	
	template< class... Args1>
	struct zip 
	{
		template<class... Args2>
		struct with
		{
			typedef Tuple<Pair<Args1,Args2>...> type;
			//Pair<Args1,Args2>... 是包展开  ,Pair<Args1,Args2>是模式
		};
	};

	typedef zip<short,int>::with<unsigned short,unsigned>::type T1;
	/*
		Pair<Args1,Args2>... 展开如下:
		Pair<short,unsigned short> , Pair<int , unsigned int>
		T1实际为Tuple<Pair<short,unsigned short> , Pair<int,unsigned int >>
	*/

对于一个模式中出现两个包(Args1 Args2),会同步展开,两个包长度必须相同。

注:单个模板参数列表中,参数包只能有一个,且必须是最后一个目标参数。如果处理两组关联的异构类型/值,需通过嵌套模板或std::tuple包装实现

  • 嵌套包展开
cpp 复制代码
template<class... Args>
void g(Args... args)
{
	f( const_cast<const Args*>(&args)...);
	// const_cast<const Args*>(&args)为模式,同时展开了Args(类型包)与args(值包) ,包长度必须相同且一一对应
}

template<class... Args>
void g(Args... args)
{
	f(h(args...) + args...);
	//内层展开 args... 展开为h(p1,p2,p3), 外层展开为 (h(p1,p2,p3) + args...)
	// (h(p1,p2,p3) + args...) 再展开为 h(p1+p2+p3) + p1 ,  h(p1+p2+p3) + p2 ,  h(p1+p2+p3) + p3
}
  • 空包展开
    当一个扩展包的元素个数为0时,称为空包。空包展开实例化不会改变其外围结构的语法解释
cpp 复制代码
template<class... Bases>
struct X : Bases... {};

template<class... Args>
void f(Args.. args) { X<Args...> x(args...); }

template void f<>();  //ok

包索引,也称为参数包下标,解决了无法直接、简洁地访问参数包中特定位置的元素

  • 在 C++20 及之前,访问包中某个元素通过 1、 递归展开 2、std::tuple + std::get
cpp 复制代码
// 递归 ,可读性差编译慢
template<size_t I, typename Head, typename... Tail>
decltype(auto) get_by_index(const Head& head, const Tail&... tail) {
    if constexpr (I == 0) return head;
    else return get_by_index<I-1>(tail...);
}

// 转成 tuple,存在拷贝或引用丢失的风险
template<typename... Args>
void foo(Args&&... args) {
    auto t = std::forward_as_tuple(std::forward<Args>(args)...);
    auto& second = std::get<1>(t);  // 访问第二个元素
}
  • C++26 包索引语法:args...[N]
    从 C++26 开始,可以直接对参数包使用下标运算符(...[]),其中索引必须是编译期常量表达式
cpp 复制代码
template<typename... Args>
void process(Args&&... args) {
    // 访问第一个元素(索引 0)
    auto&& first = args...[0];
    // 访问第三个元素
    auto&& third = args...[2];
    // 可以用 constexpr 变量或整形常量
    constexpr size_t idx = 2;
    auto&& elem = args...[idx];
    
    // 在编译期 if 中判断类型
    if constexpr (std::is_same_v<decltype(args...[0]), int>) {
        // ...
    }
}

注: C++26同时支持typename...Ts的包索引,如Ts...[0] 可以取出第一个参数列表的类型

使用场景

  • 打印任意数量参数
cpp 复制代码
template<typename T , typename... Args>
std::unique_ptr<T> createObject(Args&&... args)
{
    return std::make_unique<T>(std::forward<Args>(args)...);
}

class MyClass {
public:
    MyClass(int a, std::string b)
    {
        std::cout << "a: << " << a << " ,b: << " << b << std::endl;
    }

    MyClass(int a, std::string b ,double c)
    {
        std::cout << "a: << " << a << " ,b: << " << b << " ,c: << " << c << std::endl;
    }
};

int main()
{
    auto obj1 = createObject<MyClass>(10, "test");
    auto obj2 = createObject<MyClass>(100, "test1",20);
    auto obj3 = createObject<MyClass>(170, "test2", 20.f);
    auto obj4 = createObject<std::string>();
}
相关推荐
忆往wu前2 小时前
一文通透 Vue动态组件体系:插槽|数据监听|组件通信|动态切换|缓存—闭环
前端·面试
Cosolar2 小时前
大模型应用开发工程师 · 学习路线(完整技术栈版)
人工智能·面试·架构
书到用时方恨少!2 小时前
Python 面向对象进阶:多态——同一个接口,千种面孔
开发语言·python·多态·面向对象
徐新帅2 小时前
4181:【GESP2603七级】拆分
c++·学习·算法·信奥赛
无忧.芙桃2 小时前
现代C++精讲之处理类型
开发语言·c++
haina20192 小时前
海纳AI正式发布“面试Agent”——实现千岗千面与人机共管的智面新纪元
人工智能·面试·职场和发展
黎梨梨梨_2 小时前
C++入门基础(下)(重载,引用,inline,nullptr)
开发语言·c++·算法
谁刺我心2 小时前
[QML]Functional功能型控件-虚拟键盘
开发语言·qml·虚拟键盘
feVA LTYR2 小时前
Windows上安装Go并配置环境变量(图文步骤)
开发语言·windows·golang