【转载】泛化之美 —— C++11 可变模版参数的妙用(下)

参考原文地址: 泛化之美 —— C++11 可变模版参数的妙用

我对文章的格式和错别字进行了调整,并补充并标注出了重要的部分。以下是正文。

正文

可变参数模板类

可变参数模板类是一个带可变参数的模板类,比如 C++11 中的元组 std::tuple 就是一个可变模板类,它的定义如下:

1
2
cpp复制代码template <class... Types>
class tuple;

这个可变参数模板类可以携带任意类型任意个数的模板参数:

1
2
3
cpp复制代码std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5, "");

可变参数模板类的参数个数可以为 0 个,所以下面的定义也是也是合法的:

1
cpp复制代码std::tuple<> tp;

可变参数模板类的参数包展开的方式和可变参数模板函数的展开方式不同,可变参数模板类的参数包展开需要通过模板特化继承方式去展开,展开方式比可变参数模板函数要复杂。下面我们来看一下展开可变参数模板类中的参数包的方法。

模版偏特化和递归方式来展开参数包

可变参数模板类的展开一般需要定义两到三个类,包括类声明偏特化的模板类。如下方式定义了一个基本的可变参数模板类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
cpp复制代码// 前向声明
template <typename... Args>
struct Sum;

// 基本定义
template <typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

// 递归终止
template <typename Last>
struct Sum<Last>
{
enum
{
value = sizeof(Last)
};
};

这个 Sum 类的作用是在编译期计算出参数包中参数类型的 size 之和,通过 Sum<int, double, short>::value 就可以获取这 3 个类型的 size 之和为 14。这是一个简单的通过可变参数模板类计算的例子,可以看到一个基本的可变参数模板应用类由三部分组成:

第一部分是:

1
2
cpp复制代码template <typename... Args>
struct sum

它是前向声明,声明这个 Sum 类是一个可变参数模板类;

第二部分是类的定义:

1
2
3
4
5
6
7
8
cpp复制代码template <typename First, typename... Rest>
struct Sum<First, Rest...>
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

它定义了一个部分展开的可变参数模板类,告诉编译器如何递归展开参数包。

第三部分特化的递归终止类

1
2
3
4
5
6
7
8
cpp复制代码template <typename Last>
struct sum<last>
{
enum
{
value = sizeof(First)
};
}

通过这个特化的类来终止递归:

1
2
cpp复制代码template <typename First, typename... Args>
struct sum;

这个前向声明要求 Sum 的模板参数至少有一个,因为可变参数模板中的模板参数可以有 0 个,有时候 0 个模板参数没有意义,就可以通过上面的声明方式来限定模板参数不能为 0 个。上面的这种三段式的定义也可以改为两段式的,可以将前向声明去掉,这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cpp复制代码template <typename First, typename... Rest>
struct Sum
{
enum
{
value = Sum<First>::value + Sum<Rest...>::value
};
};

template <typename Last>
struct Sum<Last>
{
enum
{
value = sizeof(Last)
};
};

上面的方式只要一个基本的模板类定义和一个特化的终止函数就行了,而且限定了模板参数至少有一个。

递归终止模板类可以有多种写法,比如上例的递归终止模板类还可以这样写

1
2
3
4
5
6
7
8
9
10
cpp复制代码template <typename... Args>
struct sum;
template <typename First, typenameLast>
struct sum<First, Last>
{
enum
{
value = sizeof(First) + sizeof(Last)
};
};

在展开到最后两个参数时终止。

还可以在展开到 0 个参数时终止:

1
2
3
4
5
6
7
8
cpp复制代码template <>
struct sum<>
{
enum
{
value = 0
};
};

还可以使用 std::integral_constant 来消除枚举定义 value 。利用 std::integral_constant 可以获得编译期常量的特性,可以将前面的 sum 例子改为这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp复制代码//前向声明
template <typename First, typename... Args>
struct Sum;

//基本定义
template <typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};

//递归终止
template <typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};
sum<int, double, short>::value; //值为14

继承方式展开参数包

还可以通过继承方式来展开参数包,比如下面的例子就是通过继承的方式去展开参数包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cpp复制代码//整型序列的定义
template <int...>
struct IndexSeq
{
};

//继承方式,开始展开参数包
template <int N, int... Indexes>
struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...>
{
};

// 模板特化,终止展开参数包的条件
template <int... Indexes>
struct MakeIndexes<0, Indexes...>
{
typedef IndexSeq<Indexes...> type;
};

int main()
{
using T = MakeIndexes<3>::type;
cout << typeid(T).name() << endl;
return 0;
}

其中 MakeIndexes 的作用是为了生成一个可变参数模板类的整数序列,最终输出的类型是:struct IndexSeq<0,1,2>

MakeIndexes 继承于自身的一个特化的模板类,这个特化的模板类同时也在展开参数包,这个展开过程是通过继承发起的,直到遇到特化的终止条件展开过程才结束。 MakeIndexes<1,2,3>::type 的展开过程是这样的:

1
2
3
4
5
6
cpp复制代码MakeIndexes<3> : MakeIndexes<2, 2> {}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2> {}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2>
{
typedef IndexSeq<0, 1, 2> type;
}

通过不断的继承递归调用,最终得到整型序列 IndexSeq<0, 1, 2>

如果不希望通过继承方式去生成整型序列,则可以通过下面的方式生成。

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码template <int N, int... Indexes>
struct MakeIndexes3
{
using type = typename MakeIndexes3<N - 1, N - 1, Indexes...>::type;
};

template <int... Indexes>
struct MakeIndexes3<0, Indexes...>
{
typedef IndexSeq<Indexes...> type;
};

我们看到了如何利用递归以及偏特化等方法来展开可变模板参数,那么实际当中我们会怎么去使用它呢?我们可以用可变模版参数来消除一些重复的代码以及实现一些高级功能,下面我们来看看可变模版参数的一些应用。

可变参数模板消除重复代码

C++11 之前如果要写一个泛化的工厂函数,这个工厂函数能接受任意类型的入参,并且参数个数要能满足大部分的应用需求的话,我们不得不定义很多重复的模版定义,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
cpp复制代码template <typename T>
T *Instance()
{
return new T();
}

template <typename T, typename T0>
T *Instance(T0 arg0)
{
return new T(arg0);
}

template <typename T, typename T0, typename T1>
T *Instance(T0 arg0, T1 arg1)
{
return new T(arg0, arg1);
}

template <typename T, typename T0, typename T1, typename T2>
T *Instance(T0 arg0, T1 arg1, T2 arg2)
{
return new T(arg0, arg1, arg2);
}

template <typename T, typename T0, typename T1, typename T2, typename T3>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3)
{
return new T(arg0, arg1, arg2, arg3);
}

template <typename T, typename T0, typename T1, typename T2, typename T3, typename T4>
T *Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
return new T(arg0, arg1, arg2, arg3, arg4);
}
struct A
{
A(int) {}
};

struct B
{
B(int, double) {}
};
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);

可以看到这个泛型工厂函数存在大量的重复的模板定义,并且限定了模板参数。用可变模板参数可以消除重复,同时去掉参数个数的限制,代码很简洁, 通过可变参数模版优化后的工厂函数如下:

1
2
3
4
5
6
7
cpp复制代码template <typename… Args>
T *Instance(Args &&… args)
{
return new T(std::forward<Args>(args)…);
}
A *pa = Instance<A>(1);
B *pb = Instance<B>(1, 2);

可变参数模板实现泛化的 delegate

C++ 中没有类似 C# 的委托,我们可以借助可变模版参数来实现一个。C# 中的委托的基本用法是这样的:

1
2
3
4
5
6
7
8
9
cpp复制代码delegate int AggregateDelegate(int x, int y); //声明委托类型

int Add(int x, int y) { return x + y; }
int Sub(int x, int y) { return x - y; }

AggregateDelegate add = Add;
add(1, 2); //调用委托对象求和
AggregateDelegate sub = Sub;
sub(2, 1); // 调用委托对象相减

C# 中的委托的使用需要先定义一个委托类型,这个委托类型不能泛化,即委托类型一旦声明之后就不能再用来接受其它类型的函数了,比如这样用:

1
2
3
4
cpp复制代码int Fun(int x, int y, int z) { return x + y + z; }
int Fun1(string s, string r) { return s.Length + r.Length; }
AggregateDelegate fun = Fun; //编译报错,只能赋值相同类型的函数
AggregateDelegate fun1 = Fun1; //编译报错,参数类型不匹配

这里不能泛化的原因是声明委托类型的时候就限定了参数类型和个数,在 C++11 里不存在这个问题了,因为有了可变模版参数,它就代表了任意类型和个数的参数了,下面让我们来看一下如何实现一个功能更加泛化的 C++ 版本的委托(这里为了简单起见只处理成员函数的情况,并且忽略 constvolatileconst volatile 成员函数的处理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
cpp复制代码template <class T, class R, typename... Args>
class MyDelegate
{
public:
MyDelegate(T *t, R (T::*f)(Args...)) : m_t(t), m_f(f) {}

R operator()(Args &&...args)
{
return (m_t->*m_f)(std::forward<Args>(args)...);
}

private:
T *m_t;
R (T::*m_f)
(Args...);
};

template <class T, class R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T *t, R (T::*f)(Args...))
{
return MyDelegate<T, R, Args...>(t, f);
}

struct A
{
void Fun(int i) { cout << i << endl; }
void Fun1(int i, double j) { cout << i + j << endl; }
};

int main()
{
A a;
auto d = CreateDelegate(&a, &A::Fun); //创建委托
d(1); //调用委托,将输出1
auto d1 = CreateDelegate(&a, &A::Fun1); //创建委托
d1(1, 2.5); //调用委托,将输出3.5
}

MyDelegate 实现的关键是内部定义了一个能接受任意类型和参数个数的 “万能函数”:R (T::*m_f)(Args...),正是由于可变模版参数的特性,所以我们才能够让这个 m_f 接受任意参数。

总结

使用可变模版参数的这些技巧相信读者看了会有耳目一新之感,使用可变模版参数的关键是如何展开参数包,展开参数包的过程是很精妙的,体现了泛化之美、递归之美,正是因为它具有神奇的“魔力”,所以我们可以更泛化的去处理问题,比如用它来消除重复的模版定义,用它来定义一个能接受任意参数的 “万能函数” 等。其实,可变模版参数的作用远不止文中列举的那些作用,它还可以和其它 C++11 特性结合起来,比如 type_traitsstd::tuple 等特性,发挥更加强大的威力,将在后面模板元编程的应用中介绍。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%