Understanding variadic templates
1
GP and Templates(泛型编程与模板)
2
Ellipsis operator and Parameter pack(省略操作符与参数包)
1template <class... Args>
2void func(Args... args)
3{
4 std::cout << sizeof...(args) << std::endl;
5}
"…"叫做省略操作符(ellipsis operator),用以表示任意个参数,带省略号的参数称为参数包(Parameter pack)。
当省略操作符出现在参数名(args)左边时,标示着参数是一个参数包;出现在参数名右边时,用于扩展参数包。因为我们无法直接获取参数包args中的每个参数,所以只能通过展开的方式来获取,那么如何展开参数包就成了难点所在。
现在func函数可以接受任意个参数:
1func<> f1; // 0个参数
2func<int> f2; // 1个参数
3func<int, float, string> f3; // 3个参数
4func<long, vector<int>, string, bool> f4; // 4个参数
5// ...etc.
若想知道可变参数模板中的参数个数,可以借助sizeof…()操作符(不是sizeof)。一般情况下并不会使用该操作符,只有当你想要对参数包中的某个参数进行特殊处理的情况下才需要使用。
3
Function template and Class template(函数模板和类模板)
函数模板和类模板都支持可变参数模板,可以称它们为可变参数函数模板和可变参数类模板,不过,统称为可变参数模板。
在可变参数模板中,变化得不仅仅是参数的个数,还有参数的类型。
利用参数逐一递减的特性,可以使用每个不同类型参数,这个展开参数的过程,是可变参模板的核心所在。
如前文所述,在泛型编程中的拆解问题的手法依赖于递归,而由于函数模板不支持偏特化,所以它与类模板展开参数包的方式也不尽相同。
3.1
Function template
我们先来看如何使用可变参数函数模板。
程序开发中,免不了需要调试,比如一个游戏程序,在客户端运行时需要收集日志,方便后期跟踪调试。
平时大家都喜欢用printf调试法,直接将调试信息输出到控制台窗口上。可惜的是,若程序为Win32窗体程序,这种方法便没用,因为没有控件台用于输出。而若在MSVC调试器之下运行程序,可以使用OutputDebugString函数,向MSVC的调试主控台打印信息。
为方便打印,使用起来需要向printf那样简单,所以可以用可变参数函数模板来实现:
1#include <Windows.h>
2#include <stdio.h>
3
4template <typename... Args>
5int DebugPrintF(const char* format , Args... args)
6{
7 const unsigned int MAX_CHARS = 1023;
8 static char buffer[MAX_CHARS + 1];
9
10 int charsWritten = snprintf(buffer, MAX_CHARS, format, args...);
11 buffer[MAX_CHARS] = '\0';
12
13 OutputDebugString(buffer);
14 return charsWritten;
15}
16
17int main()
18{
19 DebugPrintF("[%s] [%s] %s:%d %s %dh", "2020-04-20 19:45:00", "INFO", "127.0.0.1:", 8006, "online time:", 3);
20
21 return 0;
22}
这种实现的确可以使用,然而你发现还是使用snprintf,相当于仅是换身衣服罢了。
若不想用这种实现,那么来看第二种实现:
1void PrintToSomewhere(const char* value)
2{
3 OutputDebugString(value);
4}
5
6void PrintToSomewhere(int value)
7{
8 char buf[20];
9 _itoa_s(value, buf, 10);
10 OutputDebugString(buf);
11}
12
13void PrintToSomewhere(const char value)
14{
15 char buffer[2];
16 buffer[0] = value;
17 buffer[1] = '\0';
18 OutputDebugString(buffer);
19}
20
21template <typename T>
22void DebugPrintF(T s)
23{
24 while (*s)
25 {
26 if (*s == '%' && *(++s) != '%')
27 throw std::runtime_error("invalid s string: missing arguments");
28 //std::cout << *s++;
29 PrintToSomewhere(*s++);
30 }
31}
32
33template <typename T, typename... Args>
34void DebugPrintF(const char* s, T value, Args... args)
35{
36 while (*s)
37 {
38 if (*s == '%' && *(++s) != '%')
39 {
40 // 输出到指定位置
41 PrintToSomewhere(value);
42
43 DebugPrintF(++s, args...);
44 return;
45 }
46 //std::cout << *s++;
47 PrintToSomewhere(*s++);
48 }
49 throw std::logic_error("extra arguments provided to DebugPrintF");
50}
该版本改自网上流传的一个使用可变参数模板实现printf的例子,这里职责分为两个:
抓出传进来的参数
进行输出
DebugPrintF负责第一个任务,它只需要将传进来的东西一个一个地抓出来,实际的输出任务交给PrintToSomewhere,这个函数可以自由实现,可以自定义输出流,不论是文件也好,绘图也罢,都取决于具体的需求。
所以这里不关心PrintToSomewhere,主要来看DebugPrintF。
它分成两个版本,一个参数的版本用于终止递归,三个参数的版本用于完成主要任务。
三个参数的版本中,第一个参数为格式化字符串,第二个参数为实际的首个参数,参数包为其余的实际参数。
每次进来,都会检查格式化字符,将其替换为实际字符。输出后,指针后移,循此往复,以递归来不断分解问题,最终,来到一个参数的版本,标示问题解决。
3.2
Class template
因为类模板支持偏特化,所以可变参类模板可以利用该特性来展开参数包。
还是先来看一个简单的例子,这个例子用于获得参数包中参数类型的大小之和,类型可以是任意个:
1template <typename... Args> struct TypeSize;
2
3template <typename Last>
4struct TypeSize<Last>
5{
6 enum { value = sizeof(Last) };
7};
8
9template <typename First, typename... Args>
10struct TypeSize<First, Args...>
11{
12 enum { value = TypeSize<First>::value + TypeSize<Args...>::value };
13};
之后可以这样使用:
1TypeSize<int>::value; // value: 4
2TypeSize<int, double, bool>::value; // value: 13
3TypeSize<std::string, float>::value; // value: 36
4TypeSize<std::string>::value; // value: 32
这里利用了C++泛型编程的工具之一:编译期整数计算。再加上递归,便能在编译期完成计算类型大小的工作。
因为我们的递归终止条件中有一个参数,所以TypeSize无法接受0个模板参数,实际上,那样也毫无意义。
当然,若你执意如此,也未为不可,递归的终止条件完全由你决定,比如上面的递归终止条件还可以写成这样:
1// 0个参数终止
2template <>
3struct TypeSize<>
4{
5 enum { value = 0 };
6};
7
8// 2个参数终止
9template <typename First, typename Second>
10struct TypeSize<First, Second>
11{
12 enum { value = sizeof(First) + sizeof(Second) };
13};
14
15// ...etc.
我们还可以使用C++11的integral_constant来消除enum:
1template<typename First, typename... Args> struct TypeSize;
2
3template <typename First, typename... Args>
4struct TypeSize<First, Args...>
5 : std::integer_constant<int, TypeSize<First>::value + TypeSize<Args...>::value>
6{};
7
8template<typename Last>
9struct TypeSize<Last> : std::integer_constant<int, sizeof(Last)>
10{};
11
12TypeSize<int, double, short>::value; // output: 14
interal_constant可以包覆任意类型的一个静态常量,它的定义如下:
1template<class T, T v>struct integral_constant {
2 static constexpr T value = v;
3 using value_type = T;
4 using type = integral_constant; // using injected-class-name
5 constexpr operator value_type() const noexcept { return value; }
6 constexpr value_type operator()() const noexcept { return value; } //since c++14
7};
通过其中的编译期静态常量value,便能以递归的方式得到每个类型的大小,使用方式和上面的方式完全一样。
4
Recursive inheritance(递归继承)
1typedef tuple<int, double, string> my_tuple;
2my_tuple t(1, 2.7, "blah blah...");
3t.get<0>(); // 1
4t.get<1>(); // 2.7
5t.get<2>(); // blah blah...
可以看到,它可以接受任意类型、任意个数的参数,并可以访问指定位置的参数内容。
因此,下面根据需求来进行实现:
1template <typename... Types> class Tuple;
2template<> class Tuple<> {}
3
4template <typename Head, typename... Tail>
5class Tuple<Head, Tail...> : private Tuple<Tail...>
6{
7 using TailType = Tuple<Tail...>;
8public:
9 Tuple() {}
10 Tuple(Head v, Tail... vtails) : head_(v), TailType(vtails...) {}
11
12protected:
13 Head head_;
14
15public:
16 Head head() { return head_; }
17 TailType& tail() { return *this; }
18};
Tuple支持可变参数的模板,因此可以接受任意个数、任意类型的参数。 通过特化,提供了空类型的Tuple<>,该类型可用于终止递归。 主版本的Tuple,将传递进来的参数分为第一个和其余个(1, N)。这样每次传递进来的参数会依次减少,达到遍历所有参数的效果。 主版本的Tuple通过私有继承,递归式地继承自后一个Tuple,也就是参数逐次递减的下一个Tuple,我们将其称为尾部Tuple。 私有继承表示has-a关系,继承其实现;公有继承表示is-a关系,继承其接口。表示父子关系或访问接口时使用公有继承,则处应该是has-a关系,所以使用私有继承。 因为要访问传入的数据,所以必须添加一个成员变量来进行保存,这个变量为head——每次传入Tuple中的第一个参数类型所定义的数据。 利用head()来访问传入的数据,利用tail()来访问其余的数据。
------------------------------
Tuple<> |
------------------------------
↑
------------------------------
Tuple<string> |
string head_("blah blah...");|
------------------------------
↑
------------------------------
Tuple<float, string> |
float head_(2.0); |
------------------------------
↑
------------------------------
Tuple<int, float, string> |
int head_(1); |
------------------------------
5
索引式访问Tuple
可以通过head和tail来访问tuple保存的数据:
1void TupleTest()
2{
3 Tuple<int, float, std::string> t(10, 2.0, "blah blah...");
4 std::cout << "sizeof(t):" << sizeof(t) << std::endl;
5
6 std::cout << t.head() << std::endl;
7 std::cout << t.tail().head() << std::endl;
8 std::cout << t.tail().tail().head() << std::endl;
9}
输出如下:
1TailType& tail() { return *this; }
TailType是(1,N)个参数中后N个参数的Tuple类型,当前Tuple继承自TailType。
那么返回当前this指针给上层Tuple,就会发生强制类型转换,当前Tuple转换为TailType,这样当前Tuple的head_成员就会消失,只有上层Tuple的head_。此时通过head()调用得到的是上层Tuple的head_,也就是第二个参数。
如此递归,就能访问所有元素。
可以看到,核心是类型向上转换,那么我们只要获得指定索引的Tuple类型,就能直接把当前Tuple强制转换成需要的类型,从而访问元素。
开始提供索引式访问之前,我们需要修改Tuple类如下:
1template <typename... Types> class Tuple;
2template<> class Tuple<> {};
3
4template <typename Head, typename... Tail>
5class Tuple<Head, Tail...> : public Tuple<Tail...>
6{
7 using ValueType = Head;
8 using ReferenceType = Head&;
9 using ThisType = Tuple<Head, Tail...>;
10 using TailType = Tuple<Tail...>;
11
12public:
13 Tuple() {}
14 Tuple(Head v, Tail... vtails) : head_(v), TailType(vtails...) {}
15
16 ReferenceType head() { return head_; }
17 //TailType& tail() { return *this; }
18
19protected:
20 ValueType head_;
21};
1class A {};
2class B : private A
3{
4public:
5 void test()
6 {
7 A& a = *this; // ok! B cast to its private base class A
8 }
9};
10
11B b;
12A& a = b; // error! cannot cast B to its private base class A
1template <std::size_t I, typename... TList> struct TupleAt;
2
3template <std::size_t I, typename T, typename... TList>
4struct TupleAt<I, Tuple<T, TList...>>
5{
6 using ValueType = typename TupleAt<I - 1, Tuple<TList...>>::ValueType;
7 using TupleType = typename TupleAt<I - 1, Tuple<TList...>>::TupleType;
8};
9
10template <typename T, typename... TList>
11struct TupleAt<0, Tuple<T, TList...>>
12{
13 using ValueType = T;
14 using TupleType = Tuple<T, TList...>;
15};
16
17template<>
18struct TupleAt<0, Tuple<>>
19{
20 using ValueType = Tuple<>;
21 using TupleType = Tuple<>;
22};
1template <std::size_t I, typename... TList>
2typename TupleAt<I, Tuple<TList...>>::ValueType&
3TupleGet(Tuple<TList...>& tuple)
4{
5 using TupleType = Tuple<TList...>;
6 using BaseTupleType = typename TupleAt<I, TupleType>::TupleType;
7
8 return static_cast<BaseTupleType&>(tuple).head();
9}
外部接口只需简单的进行转换,真正的工作由TupleAt进行完成。若Tuple是私有继承,此处的转换会报错。
现在可以这样使用:
1void TupleTest()
2{
3 Tuple<int, float, std::string> t(10, 32.0, "blah blah...");
4 std::cout << "sizeof(t):" << sizeof(t) << std::endl;
5
6 // std::cout << t.head() << std::endl;
7 // std::cout << t.tail().head() << std::endl;
8 // std::cout << t.tail().tail().head() << std::endl;
9
10 std::cout << TupleGet<0>(t) << std::endl;
11 std::cout << TupleGet<1>(t) << std::endl;
12 std::cout << TupleGet<2>(t) << std::endl;
13}
6
Recursive composition(递归复合)
最后,小作补充,除了「递归继承」,上述Tuple也可以使用「递归复合」来完成。
「递归复合」不需要继承,直接在当前类中进行递归展开,实现起来如下:
1template <typename Head, typename... Tail>
2class Tuple<Head, Tail...>
3{
4public:
5 using ValueType = Head;
6 using ReferenceType = Head&;
7 using ThisType = Tuple<Head, Tail...>;
8 using TailType = Tuple<Tail...>;
9
10 Tuple() {}
11 Tuple(Head v, Tail... vtail) : head_(v), tail_(vtail...) {}
12
13 ReferenceType head() { return head_; }
14 TailType& tail() { return tail_; }
15
16protected:
17 TailType tail_;
18 ValueType head_;
19};
1Tuple<int, float, std::string> t(10, 32.0, "blah blah...");
2
3std::cout << "sizeof(t):" << sizeof(t) << std::endl;
4
5std::cout << typeid(decltype(tuple)).name() << std::endl;
6std::cout << typeid(decltype(tuple.tail())).name() << std::endl;
7std::cout << typeid(decltype(tuple.tail().tail())).name() << std::endl;
输出如下:
这里推荐使用递归继承,处理和理解起来都要容易些。
7
总结
往期推荐
Uniform Initialization and initializer_list
C++20 Coroutines:operator co_await
Demystifying C++20 Coroutines