C++11 之变长参数模板

C++ 中可以对函数重载,即同样的函数名字(其实在编译期还是会被生成不同的名字)可以有不同的参数列表,例如 STL 中 string 的构造函数:

string();
string (const string& str);
string (const string& str, size_t pos, size_t len = npos);
string (const char* s);
string (const char* s, size_t n);
...

看上去和 C 中的变长参数有点类似:

void func(int nil, ...);

有点不同的是,C 的变长参数要求至少有一个指定的参数(这是为了用 va_start 确定变长参数列表的起始位置),变长参数列表位于最后;而 C++ 中的重载函数没有这个限制,也有参数类型检查,可以在编译期发现更多的错误。

除了显式的函数重载外,模板其实也是另一种重载的方法:

template<class T>
void func(T t);

不管是重载还是模板,都需要在编译期明确指定函数原型(这与 C++ 是编译型静态语言有关),对于像“实现一个函数 g,g 的参数是另一个函数 f 和 f 需要的参数,在 g 中实现对 f 的调用”这样的需求就无能为力了,因为不同的 f 的参数列表不一样,而原来的模板语法和函数重载都不可能穷举所有的情况,这样就不能在编译期生成对应的函数。

不知道变长参数模板是不是在这样的情况下产生的,不过它的出现倒是解决了我在写 lua-cpp 时遇到的上面提到的问题,所以就从这个角度介绍一下使用方法。

先看一下变长参数模板的定义方法:

#include <iostream>
using namespace std;

template<typename... Argv>
void func(Argv... argv)
{
    cout << "func() is called with " << sizeof...(Argv)
        << "argument(s)." << endl;
}

int main(void)
{
    func();
    func(5, "hello");
    func("hello", "ouonline", "!");

    return 0;
}

先看一下 func() 的模板定义。为了定义变长参数列表,C++ 11 增加了一种模板语法:

template<typename... Argv>

使用三个点“...”来表示不定参数,这个和 C 中的变长参数列表倒是一致,意思是说这里接受的参数个数和类型都是不确定的。“Argv”表示参数集合的类型,不同的参数所组成的集合的类型是不一样的。我们把可变的参数集合看成是一种类型而不是多个独立的参数,它在本质上和单个参数是一样的,这样理解起来会容易些。

然后是函数原型:

void func(Argv... argv);

这里用了“...”表示变长参数列表,而 argv 就是传递过来的参数列表集合。函数里用了“sizeof...(Argv)”获取传递的函数参数个数,同样的用了“...”对 sizeof 的语义进行了扩展。后面还能看到更多的“...”语义扩展,就不重复说了。

在 C 中变长参数可以通过用 va_* 系列的宏一个个地解析,但是在变长参数模板中,由于对模板参数的解包是由编译器完成的(模板实例化),因此不能通过简单的循环来解析,只好通过递归函数来完成:

#include <iostream>
using namespace std;

void print()
{
    cout << endl;
}

template<typename First, typename... Rest>
void print(const First& first, const Rest&... rest)
{
    cout << first;
    print(rest...);
}

int main(void)
{
    print();
    print(5, ", ", 6, ", ", 7);
    print("Hello", ", ", "ouonline", "!");

    return 0;
}

调用的时候不带任何参数或者递归调用到最后一步的时候都会调用不带参数的 print(),其它情况下都会调用

template<typename First, typename... Rest>
void print(const First& first, const Rest&... rest);

把参数依次解包并打印。

最后看一下用变长参数模板怎样解决上面提到的函数调用的问题:

#include <iostream>
using namespace std;

template<typename FuncType, typename... Argv>
void function_caller(FuncType f, Argv... argv)
{
    f(argv...);
}

void t1(int)
{
    cout << "t1 is called." << endl;
}

void t2(const char*, int)
{
    cout << "t2 is called." << endl;
}

int main(void)
{
    function_caller(t1, 10);
    function_caller(t2, "hello", 5);

    return 0;
}

因为“argv”其实是所有参数的集合,所以直接把“argv...”作为参数传递给 f()。例如对于 t2() 的调用,编译器在编译的时候会替我们生成对应的调用形式:

void function_caller(void (*f)(const char*, int), const char* v1, int v2)
{
    f(v1, v2);
}

关于参数转发的情况还存在右值引用这样到现在还没搞清楚的问题,就不展开细说了……

虽然有了变长参数模板,但是模板的实例化还是在编译期,变长参数模板只是提供了一种描述怎样生成函数的方法。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注