7 - 可变参数模板

有时候函数中的参数个数不确定,这时候就需要用到C++的可变参数模板。需要用形参包实现可变参数模板,然后用C++17的折叠表达式进行更轻松的形参包展开。

形参包

基本概念

形参包在C++11时引入,常见的写法如下:

template<typename... Tys>
void fn(Tys... args)
{
	// ...
}

这种函数允许任意个数的任意类型参数传入。其中,Tys... args是函数形参包,用于存放传入的全部参数;typename... Tys是类型形参包,用于存放这些参数的类型。

形参包展开

需要通过 形参包展开 使用其存储的参数。例如这样使用:

void f(const char*, int, double) { std::cout << "值\n"; }
void f(const char**, int*, double*) { std::cout << "&\n"; }

template<typename... Tys>
void fn(Tys... args)
{
    // 形参包展开
	f(args...);
	f(&args...);
}

int main()
{
	fn("test", 1, 1.2);

	return 0;
}

通过使用形参包展开,fn()里的两个f()相当于:

f(args0, args1, args2);
f(&args0, &args1, &args2);

其中,形如xxxx...的格式被称为 模式,意思就是会重复xxxx内容。

接下来用形参包展开写一个print()函数:

template <typename... Tys>
void print(const Tys&... args)
{
	int _[] = {(std::cout << args << ' ', 0)...};
}

其中,模式(std::cout << args << ' ', 0)展开后为:

(std::cout << args0 << ' ', 0),
(std::cout << args1 << ' ', 0),
(std::cout << args2 << ' ', 0)

逗号表达式会返回最右边表达式的值,这里会在输出args后给数组_初始化一个0元素。

获取形参个数

可以通过sizeof...(args)获取形参包中形参的个数:

template <typename... Tys>
void print(const Tys&... args)
{
	int _[] = {(std::cout << args << ' ', 0)...};
	std::cout << "\n个数为: " << sizeof...(args);
}

例子

输出指定下标数组元素

看看如下函数:

template<typename T, std::size_t N, typename... Tys>
void f(const T(&array)[N], Tys... idx)
{
	// 还是之前写的print(...args)
	print(array[idx]...);
}

int main()
{
	int arr[] = {1, 2, 3, 4, 5};
	f(arr, 0, 2, 4);

	return 0;
}

其中,const T(&array)[N]是一个元素个数为N,类型为Tconst数组引用。该函数将逐个输出下标为idx的元素。

求和

template<typename ...Tys, typename RT = std::common_type_t<Tys...>>
RT sum(const Tys& ...args)
{
	RT _[] = {static_cast<RT>(args)...};
	return std::accumulate(std::begin(_), std::end(_), RT {});
}

int main()
{
	double ret = sum(1, 2, 3, 0.5, 0.6);
	std::cout << ret;

	return 0;
}

RT是返回值类型,这里使用std::common_type_t<Tys...>,它可以推断出所有Tys类型中的共用类型。接下来通过形参包展开将args转换为RT类型,并求和。

折叠表达式

C++17折叠表达式让我们能以更加轻松地进行形参包展开。

引例

还是之前写过的print()

template <typename... Tys>
void print(const Tys&... args)
{
	int _[] = {(std::cout << args << ' ', 0)...};
}

为了输出形参,还定义了一个无用的整数数组,浪费空间且不美观。使用折叠表达式的写法如下:

template <typename ...Tys>
void print(Tys ...args)
{
	((std::cout << args << ' '), ...);
}

这是 一元右折叠 的写法,它不需要创建数组。

语法

一元折叠

一元折叠有两种形式,一元左折叠和一元右折叠。这里用例子熟悉一下概念:

// 一元左折叠: (... 运算符 形参包)
template<int ...I>
constexpr int v_left = (... - I);	

// 一元右折叠: (形参包 运算符 ...)
template<int ...I>
constexpr int v_right = (I - ...);

int main()
{
	std::cout << v_left<4, 5, 6> << '\n';	// -7
	std::cout << v_right<4, 5, 6> << '\n';	// 5
	return 0;
}

两者的运算结果不同,来看看他俩的展开形式:

v_left<4, 5, 6>  => ((4 - 5) - 6) = -7
v_right<4, 6, 6> => (4 - (5 - 6)) = 5

可以发现,左折叠先算左边的,右折叠先算右边的。

二元折叠

二元折叠也有两种形式,看看例子:

// 二元左折叠: (初值 运算符 ... 运算符 形参包)
template <int ...I>
constexpr int v2_left = (10 + ... + I);

// 二元右折叠: (形参包 运算符 ... 运算符 初值)
template <int ...I>
constexpr int v2_right = (I + ... + 10);

int main()
{
    // (((10 + 1) + 2) + 3) + 4
	std::cout << v2_left<1, 2, 3, 4> << '\n';
    
    // 1 + (2 + (3 + (4 + 10)))
	std::cout << v2_right<1, 2, 3, 4> << '\n';	
 
	return 0;
}

可以发现和一元折叠类似,不过有了初始值。

参考资料

  • Modern-Cpp-templates-tutorial/md/第一部分-基础知识 at main · Mq-b/Modern-Cpp-templates-tutorial (github.com)
  • C++20高级编程