技术: C++11变长参数模板

这个应该算是C++11, 即2.0时代的新特性吧, 比较有意思.(tuple中也有使用)

简介

最早在printf的时候接触过变长参数(参数个数不确定), C++11也采用了包扩展来支持模板支持任意参数. 所谓的包扩展, 就是指一组参数, 数量不确定(取决于你调用传入的时候传入的参数), 类型也是.

我一般把它简单理解成数组, 方便.

具体的例子:

1
2
3
4
template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){
//省略实现
}

可变参数, 一般放在参数列表末尾, 否则编译器报错.

二义性问题

重载, 会不会存在二义性问题呢?
例如:

1
2
3
4
5
6
template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){}

//对比
template<typename T, typename... Types>
void print(const Types&...args){}

对于编译器而言, 编译器总是找最佳匹配, 所以他们是可以共存的, 而且优先调用特化的版本, 即第一个版本.

使用场景

函数递归调用

大家一般使用这样的变长模板参数, 主要是用于递归的场景.

例如上面变参模板可以这样实现递归调用:

1
2
3
4
5
6
7
void print(){}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&...args){
cout << firstArg << endl;
print(args...);
}

最后一次递归调用会调用到 void print() 这个版本(递归一定要设置终止条件的), 没有的话, 就编译报错了.

主要用途, 用于帮助递归, 分解不定个数的参数. (使用递归(recursive)处理包(pack)参数)

上面那个例子只是用于演示, 没有实际作用, 但是它也演示了, 在使用变参模板的重载时, 如果有最佳匹配, 那么总是以最佳匹配优先(严格匹配优先, 特化匹配优先), 没有上述情况的时候, 才会匹配泛化的函数模板.

递归继承

tuple类的实现(你可以简单的把tuple看做一组对象组合在一起形成一个对象), 采用的就是递归继承.

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Head, typename...Tail>
class tuple<Head, Tail...> : private tuple<Tail>
{
typedef tuple<Tail...> inherited;
public:
tuple(){}
tuple(Head v, Tail...vtail): m_head(v), inherited(vtail...){}

inherited& tail() { return *this; }
protected:
Head m_head;
}

从上面, 你也可以看出整个tuple的构建过程, 每次继承, 只保留Head部分, 然后用this指针指向父类的部分, 即tail部分, 这就是递归的过程.

举个具体的例子:

1
tuple<int, float, string> t(1, 2.0, "hello");

继承关系是怎么样的呢?

1
2
3
4
5
6
7
tuple<> 
<--
tuple<string>
<--
tuple<float, string>
<--
tuple<int, float, string>

本类只保存第一个参数, 当做head, 然后实现这个类, 该类的父类被当做tail部分, 依次递归下去.

你可以当做组合关系, 一层层组装, 每一层只和相邻的层有关系(区分一下和继承图的关系):

试试看下面的代码:

1
2
3
4
5
6
tuple<int, float, string> t(1, 2.0, "hello");
t.head(); //1
t.tail(); //{2.0, "hello"}, 即inherited类型引用, 亦即tuple<float, string>
// 可以取地址试试看
&(t.tail())
t.tail().head(); //2.0

递归组合

如果你不喜欢用继承, 可以把tuple改成组合的方式, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//先写递归结束条件, 因为是模板, 所以可以同时存在多个(参数不同)
template<typename...Values>
class tup{};

//终止条件不需要泛型参数了
template<>
class tup<>{};

//优先调用特化版本的
template<typename Head, typename...Tail>
class tup<Head, Tail...>
{
//using composited = tup<Tail...>;
typedef tup<Tail...> composited;
protected:
composited m_tail; //其他部分用成员表示, 而不是继承方式了
Head m_head;
public:
tup()=default;
tup(Head v, Tail...vtail):m_tail(vtail...), m_head(v){}

Head head() {return m_head;}
composited& tail() {return m_tail;}
};

调用逻辑还是一样的,

1
2
3
tup<int, float, string> tt(3, 2.0, "merlin");
cout << tt.head() << endl;
cout << tt.tail().head() << endl;

单独使用

好像泛型参数模板使用的时候, 都要指定具体的类型么?

例如我下面有个泛型参数模板, 如下:

1
2
3
4
5
6
7
8
9
10
11
//递归终止时的调用
int maximum(int n)
{
return n;
}

template<typename...T>
int maximum(int first, T...others)
{
return std::max(first, maximum(others...));
}

使用的时候, 不指定泛型参数, 如cout << maximum(1,2,3,4,5) << endl;可否?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

using namespace std;

int maximum(int n)
{
return n;
}

template<typename...T>
int maximum(int first, T...others)
{
return std::max(first, maximum(others...));
}

int main()
{
cout << maximum(1,2,3,4,5) << endl; //5

return 0;
}

完整代码自己可以试一下, ok的 (调用过程是, 5Vs4, 然后从右边不断向左推进比较).

使用可变参数模板时, 不必为了专门的去特化而指定<>里的内容, 也没法指定; 变参泛型模板自己会推导

上面的例子, 同类型的不定参数最好用initializer_list .

注意事项

这个变参...的写法, 不仅仅是函数参数一项, 所以每次到底是在参数前面, 还是在参数后面, 非常烦人.

例如上面的:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename Head, typename...Tail>
class tuple<Head, Tail...> : private tuple<Tail>
{
typedef tuple<Tail...> inherited;
public:
tuple(){}
tuple(Head v, Tail...vtail): m_head(v), inherited(vtail...){}

inherited& tail() { return *this; }
protected:
Head m_head;
}

当然, 当初设计它的人, 绝对不是神经病, 所以还是有规律的, 简单记忆如下:

  • 跟在typename后面(即使类型出现), 其他情况只要类型出现, 一定跟在类型(引用)后面;
  • 使用的时候(比如函数调用)放在具体实参后面, 例如args....
  • sizeof出现, 跟在sizeof的后面, sizeof...(args)可以拿到参数个数

其实总是跟在别人后面, 只是不停的在换老大, 有typename的时候跟在typename后面

文章目录
  1. 1. 简介
  2. 2. 二义性问题
  3. 3. 使用场景
    1. 3.1. 函数递归调用
    2. 3.2. 递归继承
    3. 3.3. 递归组合
    4. 3.4. 单独使用
  4. 4. 注意事项
|