可变参数模板

可变参数模板(C++11)

上周末实现双向链表时参照了STL中的list链表,在查阅文档时发现list下的emplace方法的模板参数是template< class... Args >
当时就很疑惑,不知道这是什么参数,在网上学习了他人的博客后在此写下自己的见解。

可变参数的概念

可变参数即可以接收多个参数,如C语言中的printf函数

和scanf函数

其中的...便是可变参数列表,使函数可以接受多个类型不同的参数printf("%f%d", 2.54, 't');

可变参数模板的概念

C++11引入了可变参数模板,将可变参数应用到模板上,使得模板可以接收多个参数.

声明一个可变参数模板:

template <typename... Args>
void function(Args... args);

其中省略号有两个含义:

  1. typename... Args表示声明模板参数包,该模板可接收多个类型不同的参数。
  2. Args... args表示声明一个参数包,这个参数包中可以包含0到N(N>=0)个模板参数;

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取;而可变参数模板又可应用到函数和类中,并且模版函数不支持偏特化,所以可变参数模版函数和可变参数模版类的展开方法还不尽相同。

可变参数模板函数

使用sizeof...()运算符可以计算参数包的大小:

template <typename ...Args>	//模板参数包
void ShowSize(Args... args)	//函数形参参数包
{
	std::cout << sizeof...(args) << std::endl;	//sizeof...运算符用来查询参数包中的元素个数
}

ShowSize(1, 'j', 4.0);	//3

但这样并不能获得参数包中实际的参数,要获得实际的参数可以通过使用递归函数来展开参数包,也可以使用逗号表达式展开参数包。

递归函数展开参数包

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归函数的终止条件便是递归终止数。

递归展开函数,每次递归时接收一个参数和剩余的参数包

template <typename T, typename ...Args>
void ShowParameterPack(T value, Args... args)
{
	std::cout << value << " ";	//执行对参数的操作
	ShowParameterPack(args...);	//递归
}

有参递归终止函数需写成模板的形式,当参数包只剩最后一个参数时,匹配此函数从而终止递归,使用有参形式则不能传入空参数包

template <typename T>
void ShowParameterPack(constT& value)
{
	std::cout << value << std::endl;	//执行对最后一个参数的操作
}

无参递归终止函数,当参数包没有参数时匹配此函数:

void ShowParameterPack()
{
	std::cout << std::endl;	//可不进行任何操作
}

逗号表达式展开参数包

使用递归展开必须要有一个终止函数,而使用逗号表达式加初始化列表则不需要则不需要。

逗号表达式展开函数,其中{ (PrintArgs(args), 0)... }使用了初始化列表:

template <typename T>
void PrintArgs(const T& value)	//对参数进行操作的函数
{
	std::cout << value << " ";
}

template <typename ...Args>
void ShowParameterPack(Args... args)	//展开函数
{
	int arr[] = { (PrintArgs(args), 0)... };
	std::cout << std::endl;
}

在执行此函数时,由于逗号表达式的特性,会先计算左边的表达式,最后返回最后一个表达式,也就是先执行PrintArgs(args),而初始化列表{}会将表达式(PrintArgs(args), 0)...展开,从而就可以在构造数组的过程中执行对各个参数的操作并返回0,最后形成一个全是0的数组。

但这样会无法处理参数包为空的情况,若想处理此情况,还需重载一个接收空参数的展开函数:

void ShowParameterPack()
{
	std::cout << std::endl;	//可不进行任何操作
}

其实也可以不使用逗号表达式,只需在展开函数时返回一个与数组匹配的值即可:

template<class T>
int PrintArg(const T& value)
{
	std::cout << value << " ";
	return 0;
}

template<class ...Args>
void ShowParameterPack(Args... args)
{
	int arr[] = { PrintArg(args)... }; //没使用逗号表达式
	std::cout << std::endl;
}

当然执行操作的函数也可使用lambda表达式来代替:

template<class F, class... Args>
void ShowParameterPack(const F& function, Args&&...args)
{
	int arr[] = {function(args)...};
}

ShowParameterPack([](auto value) {std::cout << value << " "; return 0; } , 1, 'j', "CL");

其中使用了auto value来接收各种类型不同的参数。

可变参数模板类

带可变参数模板的模板类便是可变参数模板类,如元祖std::tuple就是一个可变参数模板类。

可变参数模板类可使用偏特化加递归继承来展开参数包。

模板偏特化加递归展开参数包

使用模板偏特化展开首先要进行前向申明一个可变参数模板类:

//前向声明
template <typename... Args>
class Expend;

随后对模板类进行偏特化,使一个参数与参数包分离,并进行递归调用,使所有参数分离:

//参数分离递归展开参数包
template <typename T, typename... Args>
class Expend<T, Args...> 
{
public:
    Expend(T value, Args... args) 
    {
        std::cout << value << std::endl;
        Expend<Args...> e(args...); //递归调用
    }
};

Expend<int, double, const char*, char> e(1, 2.5, "HeavenBurn", 'c');

最后再次对模板类进行偏特化以构成递归的终止条件,当参数包只剩最后一个参数时会调用:

//展开参数包的终止条件
template <typename T>
class Expend<T> 
{
public:
    Expend(T value) 
    {
        std::cout << value << std::endl;
    }
};

当然终止类也可以使用无参的形式,当参数包全部展开时会调用:

//展开参数包的终止条件
template <>
class Expend<> 
{
public:
    Expend() {}
};

也可以将前向声明与终止条件整合到一起,因为参数包可以接受0个参数,当参数包全部展开时就会调用原模板类:

template <typename... Args>
class Expend 
{
public:
    Expend(Args... args) {}
};

继承方式展开参数包

首先要定义一个基类:

template <typename... Args>
class Expend
{
public:
    Expend(){}
};

随后定义具有展开功能的子类继承父类,Expend继承于原始模板类,自身为可将参数分离的特化模板类,在构造时将参数包传给父类,再次发起展开,当无参数时则通过父类终止展开:

template <typename T, typename... Args>
class Expend<T, Args...> : public Expend<Args...>
{
public:
    Expend(T value, Args... args) 
        : Expend<Args...>(args...)   //将扩展参数包传给父类
    {
        std::cout << value << std::endl;
    }
};

Expend<int, double, std::string, char> e(1, 2.5, "HeavenBurn", 'c');

也可使用特化的模板类来终止展开,会优先调用:

template <>
class Expend<>
{
public:
    Expend(){}
};

折叠表达式(C++17)

二元运算符对形参包进行处理,省去使用递归,逗号表达式等方法展开参数包的步骤。折叠表达式中的运算符包含32个二元运算符:
+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*。在二元折叠中,两个运算符必须相同

语法

  1. 一元右折叠:将(E 运算符 ...) 转换为 (E 1 运算符 (... 运算符 (E N-1 运算符 E N)))
  1. 一元左折叠:将(... 运算符 E) 转换为 (((E 1 运算符 E 2) 运算符 ...) 运算符 E N)
  2. 二元右折叠: 将(E 运算符 ... 运算符 I) 转换为 (E 1 运算符 (... 运算符 (E N−1 运算符 (E N 运算符 I))))
  3. 二元左折叠:将(I 运算符 ... 运算符 E) 转换为 ((((I 运算符 E 1) 运算符 E 2) 运算符 ...) 运算符 E N)

其中E表示参数包,I表示为不含参数包的表达式。

使用示例

  1. 使用参数包和折叠表达式打印:
    template <typename... Args>
    void Print(const Args&... args)
    {
    	(std::cout << ... << args) << "\n";
    }
    
    Print(1, 3, 5, 97, 'a');	// 13597a
    
posted @ 2025-10-13 23:48  单身喵  阅读(4)  评论(0)    收藏  举报