可变模板参数(Variadic Templates)

可变模板参数

在学习模板编程时居然发现有类似于 “$…$” 的参数出现,它的作用在于代表输入的参数个数可能是 $0$ ~ $N$ ,也就是对参数进行了高度泛化。

可变函数模板展开

对于一个可变函数模板定义:

template<typename... T>
void func(T... args);

$…$的含义有二:

  1. 声明一个参数包 T… args ,这个参数包中可以包含0到任意个模板参数。
  2. 在模板定义的右边,可以将参数包展开成一个一个独立的参数。

对于其的一个简单应用:

template<typename... T>
void func(T... args)
{
    std::cout << sizeof...(args) << std::endl;  //打印参数个数
}

func(); //0
func(1,2.5,"new");    //3

但是我们在应用中无法直接获取参数包 args 中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数。

可变模板参数在C++17之前一般有两种展开方式:递归展开和逗号表达式展开。

递归函数展开

对于一个递归,我们需要注意的第一个点就是确定递归终点。

#include <iostream>

void print()
{
	std::cout << "end" << std::endl;
}

template<typename First, typename ...Args>
void print(First head, Args... rest)
{
	std::cout << "it is " << head << std::endl;
	print(rest...);
}

int main() {

	print(1, 2.5, "newest");
}

output:

it is 1
it is 2.5
it is newest
end

如何理解这个递归呢?

其实相当于每次将这个参数包的首个元素取出并输出:

print(1,2.5,"newest");
print(2.5,"newest");
print("newest");
print();

以下是一个利用可变参数列表以及递归解包求和的例子:

#include <iostream>

template<typename T>
T sum(T t)
{
	return t;
}

template<typename First, typename ...Args>
First sum(First head, Args... rest)
{
	return head + sum(rest...);
}

int main() {
	std::cout<<sum(1, 2, 3, 4, 5);  //out:15
}

逗号表达式展开参数包

我们知道,对于逗号运算符,C++会从左到右计算表达式的内容,最终返回最右边的表达式计算的值。那么运用这个性质,运用一个边长数组的初始化即可展开参数包:

{(func(args), 0) ...}

将会被展开为:

(
    (func(args0), 0),
    (func(args1), 0),
    (func(args2), 0),
    ...
)

最终在数组的初始化过程中会得到一个由0组成的数组。

#include <iostream>

template<typename T>
void printing(T t)
{
	std::cout << t << std::endl;
}

template<typename ...Args>
void expand(Args... args)
{
	int a[] = { (printing(args),0)... };
}

int main() {
	expand(1, 1.8, "newest");
}

output:

1
1.8
newest

此时如果结合lambda表达式,我们还可以简化书写printing函数:

template<class F,typename ...Args>
void expand(const F& f, Args... args)
{
	std::initializer_list<int>{
		(
			f(std::forward<Args>(args)),0
			)...
	};
}

int main() {
	expand(
		[](int i) {std::cout << i << std::endl; }, 
		1, 2, 3
	);
}

output:

1
2
3

当然上述写法仍然有一些限制,因为如此书写必须指定 $i$ 的类型,意味着参数包的类型必须是统一的,这个问题在C++14得到了解决,在C++14中允许在lambda表达式使用自动推导类型:

expand(
		[](auto i) {std::cout << i << std::endl; }, 
		1, 2, 3, "newest"
	);

output:

1
2
3
newest

幸运的是,在C++17以后增加了折叠表达式

上述expand函数模板就可以进行简写了:

// 使用折叠表达式实现的版本
template<class F, class... Args>
void expand_fold(const F& f, Args&&... args)
{
    // 使用逗号运算符的一元右折叠
    (f(std::forward<Args>(args)), ...);
}

折叠表达式的工作原理

折叠表达式允许我们对参数包应用二元运算符。C++17 支持四种形式的折叠表达式:

  1. 一元右折叠: (pack op …)
  2. 一元左折叠: (… op pack)
  3. 二元右折叠: (pack op … op init)
  4. 二元左折叠: (init op … op pack)

其中:

pack 是包含参数包的表达式

op 是二元运算符(如 +, -, *, /, &&,   , , 等)

init 是初始值

以下是一些应用:

#include <iostream>

// 求和
template<typename... Args>
auto sum(Args... args) {
	return (args + ...); // 一元右折叠
}

// 打印所有参数
template<typename... Args>
void print_all(Args... args) {
	((std::cout << args << " "), ...); // 一元右折叠与逗号运算符
	std::cout << std::endl;
}

// 检查是否所有参数都为真
template<typename... Args>
bool all(Args... args) {
	return (args && ...); // 一元右折叠
}

// 带初始值的求和
template<typename... Args>
auto sum_with_init(int init, Args... args) {
	return (init + ... + args); // 二元右折叠
}

int main() {
	std::cout << "Sum: " << sum(1, 2, 3, 4, 5) << std::endl;
	print_all("Hello", 42, 3.14, "World");
	std::cout << "All true? " << all(true, true, true) << std::endl;
	std::cout << "All true? " << all(true, false, true) << std::endl;
	std::cout << "Sum with init: " << sum_with_init(10, 1, 2, 3, 4, 5) << std::endl;

}

output:

Sum: 15
Hello 42 3.14 World
All true? 1
All true? 0
Sum with init: 25

可变参数模板类

之前我们介绍的是可变参数模板函数的展开方式,现在我们介绍可变模板参数类。

在C++11中,引入了 $tuple$ 也就是元组类型,它实际上就是一个可变参数模板类,它的声明如下:

template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // recursive tuple definition
	...
}

此时我们可以如此创建一个 tuple 对象:

std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, "");

当然,我们也可以建立一个空对象,但实际上它不是由上述声明所生成,而是一个特化版本:

emplate <>
class tuple<> { // empty tuple
public:
    constexpr tuple() noexcept = default; /* strengthened */

    constexpr tuple(const tuple&) noexcept /* strengthened */ {} // TRANSITION, ABI: should be defaulted
	...
}

std::tuple<> tp;

递归展开可变参数模板类

接下来是一个用递归展开可变参数模板类的例子:

#include <iostream>

template <class ...Args>
class Sum;

template<class First, class ...Rest>
class Sum<First, Rest...>
{
public:
	enum
	{
		value = Sum<First>::value + Sum<Rest...>::value
	};
};

template<class Last>
class Sum<Last>
{
public:
	enum  { value = sizeof(Last) };
};


int main() {

	std::cout << Sum<int, double,std::string>::value<<std::endl;
	
}

当然我们可以通过声明时也加一个First 使得我们可以设置限制参数个数必须 $\gt 0$ :

template <class ...Args>
class Sum;

template<class First, class ...Rest>
class Sum<First, Rest...>
{
public:
	enum
	{
		value = Sum<First>::value + Sum<Rest...>::value
	};
};

template<class Last>
class Sum<Last>
{
public:
	enum  { value = sizeof(Last) };
};

或者:

template<class First, class ...Rest>
class Sum
{
public:
	enum
	{
		value = Sum<First>::value + Sum<Rest...>::value
	};
};

template<class Last>
class Sum<Last>
{
public:
	enum  { value = sizeof(Last) };
};

也可以在参数个数为0时终止:

template<> 
class Sum<> {
public:
	enum {value = 0;}
};

也可以使用std::integral_constant 去除enum定义展开参数包:

//前向声明
template<class First, class... Args>
class Sum;

//基本定义
template<class First, class... Rest>
class Sum<First, Rest...> :public std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};

//递归终止
template<class Last>
class Sum<Last> :public std::integral_constant<int, sizeof(Last)>
{
};

附std::integral_constant 源码:

template<class T, T v>
struct integral_constant
{
    static constexpr T value = v;
    using value_type = T;
    using type = integral_constant; // using injected-class-name
    constexpr operator value_type() const noexcept { return value; }
    constexpr value_type operator()() const noexcept { return value; } // since c++14
};