C++可变参数的函数模板(附带实例)
有时,编写参数个数可变的函数或成员个数可变的类很有用。典型的例子包括 printf() 之类的函数(它采用一种格式和可变数量的参数)或者 tuple 之类的类。在 C++11 之前,前者只能通过可变参数宏(只允许编写类型不安全的函数)实现,而后者根本不可能实现。
C++11 引入了可变参数模板,它是具有可变数量参数的模板,可以编写具有可变数量参数的类型安全函数模板,也可以编写具有可变数量成员的类模板。在本节中,我们将介绍如何编写函数模板。
具有可变数量参数的函数称为可变参数函数,参数数量可变的函数模板称为可变参数函数模板。
下面的示例说明了前面的所有要点,它是一个可变参数函数模板,使用 operator+ 添加可变数量的参数:
实际上,编译器会根据可变参数函数模板的使用情况来生成具有不同数量参数的多个函数,因此只涉及函数重载,而不涉及任何类型的递归。然而,实现就是这样做的,就好像参数将以带有结束条件的递归方式进行处理一样。
在前面的代码中,我们可以识别到以下关键部分:
在 add() 函数中,我们基本上将第一个参数添加到其余参数的和中,这给人一种递归调用的感觉。当只剩下一个参数时,此递归结束,在这种情况下,将调用第一个 add() 函数重载(只有一个参数)并返回其参数的值。
函数模板 add() 的这个实现使得我们能编写以下类似代码:
当编译器遇到 add(1, 2, 3, 4, 5) 时,它会生成以下函数 arg1、arg2 等并不是编译器生成的实际名称),这表明实际上只涉及对重载函数的调用,而不涉及递归:
通过在我们编写的两个函数的开头添加 std::cout<<_PRETTY_FUNCTION__<<std::endl;,在运行代码时可以得到以下结果:
另一个例子是 add("hello"s, " "s, "world"s, "!"s),它生成 hello,world! 字符串。然而,std::basic_string 类型对 operator+ 有不同的重载,包括可以将字符串连接成新字符串的重载,因此我们应该也能够编写以下代码:
应该进一步补充的是,参数包也可以出现在初始化列表中,其大小可以使用 sizeof... 操作符获得。此外,正如在本节中所示,可变参数函数模板并不一定意味着编译时递归。所有这些都可以在以下示例中看到:
sizeof... 操作符可与模板参数包和函数参数包一起使用,sizeof...(a) 和 sizeof...(T) 将产生相同的值,然后,我们创建并返回一个元组。模板参数包 T 展开(带 T...)为 std::tuple 类模板的类型参数,函数参数包 a 被展开(带 a...)为使用初始化列表为元组成员赋的值。
C++11 引入了可变参数模板,它是具有可变数量参数的模板,可以编写具有可变数量参数的类型安全函数模板,也可以编写具有可变数量成员的类模板。在本节中,我们将介绍如何编写函数模板。
具有可变数量参数的函数称为可变参数函数,参数数量可变的函数模板称为可变参数函数模板。
C++可变参数函数模板的使用方式
要编写可变参数函数模板,必须执行以下步骤:- 为了满足可变参数函数模板的语义需要,需要定义一个具有固定数量参数的重载,以结束编译时递归调用(请参阅实例代码中的 [1])。
- 定义一个模板参数包,该模板参数包可以包含任意数量(包括零)的参数。这些参数可以是类型、非类型或模板(请参阅实例代码中的[2])。
- 定义一个函数参数包来保存任意数量(包括零)的函数参数。模板参数包和相应函数参数包的大小相同。这个大小可以用sizeof...操作符来确定(有关此操作符的信息,请参阅实例代码中的 [3])。
- 展开参数包,用提供的实际参数进行替换(请参阅实例代码中的 [4])。
下面的示例说明了前面的所有要点,它是一个可变参数函数模板,使用 operator+ 添加可变数量的参数:
template <typename T> // [1] overload with fixed number of arguments T add(T value) { return value; } template <typename T, typename... Ts> // [2] typename... Ts T add(T head, Ts... rest) // [3] Ts... rest { return head + add(rest...); // [4] rest... }
C++可变参数函数模板的工作原理
乍一看,前面的实现看起来像是递归,因为函数 add() 调用了自己,从某种意义上说,它是一种编译时递归,不会产生任何运行时递归和开销。实际上,编译器会根据可变参数函数模板的使用情况来生成具有不同数量参数的多个函数,因此只涉及函数重载,而不涉及任何类型的递归。然而,实现就是这样做的,就好像参数将以带有结束条件的递归方式进行处理一样。
在前面的代码中,我们可以识别到以下关键部分:
- typename...Ts 是一个模板参数包,用于指示可变数量的模板类型参数;
- Ts...rest 是一个函数参数包,指示可变数量的函数参数;
- rest... 是函数参数包的扩展。
在 add(T head, Ts...rest) 参数中,head 是参数列表的第一个元素,而 ...rest 是包含列表中其余参数(可以为零个或更多个)的包。在函数体中,rest... 是函数参数包的扩展,这意味着编译器将按元素顺序替换参数包。省略号的位置在语法上无关紧要。typename...Ts、typename...Ts 和 typename...Ts 都是等价的。
在 add() 函数中,我们基本上将第一个参数添加到其余参数的和中,这给人一种递归调用的感觉。当只剩下一个参数时,此递归结束,在这种情况下,将调用第一个 add() 函数重载(只有一个参数)并返回其参数的值。
函数模板 add() 的这个实现使得我们能编写以下类似代码:
auto s1 = add(1, 2, 3, 4, 5); // s1 = 15 auto s2 = add("hello"s, " "s, "world"s, "!"s); // s2 = "hello world!"
当编译器遇到 add(1, 2, 3, 4, 5) 时,它会生成以下函数 arg1、arg2 等并不是编译器生成的实际名称),这表明实际上只涉及对重载函数的调用,而不涉及递归:
int add(int head, int arg1, int arg2, int arg3, int arg4) {return head + add(arg1, arg2, arg3, arg4);} int add(int head, int arg1, int arg2, int arg3) {return head + add(arg1, arg2, arg3);} int add(int head, int arg1, int arg2) {return head + add(arg1, arg2);} int add(int head, int arg1) {return head + add(arg1);} int add(int value) {return value;}
通过在我们编写的两个函数的开头添加 std::cout<<_PRETTY_FUNCTION__<<std::endl;,在运行代码时可以得到以下结果:
int add(T, Ts ...) [with T = int; Ts = {int, int, int, int}] T add(T, Ts ...) [with T = int; Ts = {int, int, int}] T add(T, Ts ...) [with T = int; Ts = {int, int}] T add(T, Ts ...) [with T = int; Ts = {int}] T add(T) [with T = int]因为这是一个函数模板,所以它可以与支持 operator+ 的任意类型一起使用。
另一个例子是 add("hello"s, " "s, "world"s, "!"s),它生成 hello,world! 字符串。然而,std::basic_string 类型对 operator+ 有不同的重载,包括可以将字符串连接成新字符串的重载,因此我们应该也能够编写以下代码:
auto s3 = add("hello"s, ' ', "world"s, '!'); // s3 = "hello world!"然而,这将产生编译错误,如下所示。

编译器生成这里所示的代码,其中返回类型与第一个参数的类型相同。但是,第一个参数是 std::string 或 char(同样,为了简单起见,将 std::basic_string<char, std::char_traits<char>, std::allocator<char>> 替换为 string)。如果 char 是第一个参数类型,则返回值 head+add(...) 的类型是 std::string,它与函数返回类型不匹配,并且没有隐式转换的函数:注意,为了简单起见,我实际上用字符串 hello world!替换了std::basic_string<char,std::char_traits<char>,std::allocator<char>>。
string add(string head, char arg1, string arg2, char arg3) {return head + add(arg1, arg2, arg3);} char add(char head, string arg1, char arg2) {return head + add(arg1, arg2);} string add(string head, char arg1) {return head + add(arg1);} char add(char head, char arg1) {return head + add(arg1);} return value;我们可以通过修改可变参数函数模板来解决这个问题,这样它的返回类型就不是 T,而是 auto。在这种情况下,返回类型总是从返回表达式推断出来的,在我们的示例中,在所有情况下它都是 std::string:
template <typename T, typename... Ts> auto add(T head, Ts... rest) { return head + add(rest...); }
应该进一步补充的是,参数包也可以出现在初始化列表中,其大小可以使用 sizeof... 操作符获得。此外,正如在本节中所示,可变参数函数模板并不一定意味着编译时递归。所有这些都可以在以下示例中看到:
template<typename... T> auto make_even_tuple(T... a) { static_assert(sizeof...(a) % 2 == 0, "expected an even number of arguments"); std::tuple<T...> t { a... }; return t; } auto t1 = make_even_tuple(1, 2, 3, 4); // OK // error: expected an even number of arguments auto t2 = make_even_tuple(1, 2, 3);在前面的代码片段中,我们定义了一个函数,该函数创建了一个具有偶数个成员的元组。我们首先使用 sizeof...(a) 来确保有偶数个参数的断言,否则会产生编译错误。
sizeof... 操作符可与模板参数包和函数参数包一起使用,sizeof...(a) 和 sizeof...(T) 将产生相同的值,然后,我们创建并返回一个元组。模板参数包 T 展开(带 T...)为 std::tuple 类模板的类型参数,函数参数包 a 被展开(带 a...)为使用初始化列表为元组成员赋的值。