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
,类型为T
的const
数组引用。该函数将逐个输出下标为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高级编程