Variadic templates in C++

Variadic templates

Reference from : https://eli.thegreenplace.net/2014/variadic-templates-in-c/

在C++11之前,编写一个具有任意数量参数的参数的唯一方法就是使用variadic函数,如printf,scanf之流就是这样实现的,使用了省略语法和相关的va_ 宏定义。由于所有的类型解析都在运行时,并且必须要在va_arg中显式地使用强制转换,这些低级的内存操作,很容易带来代码的段错误。

而同样的,在C++11之前,模板的编写必须要声明固定数量的参数,无法表达具有可变数量参数的类或者函数模板。

Basic example

C++11的一个新特性就是可变参数模板,这个新特性使得我们以类型安全的方式编写接收任意数量参数的函数,并在编译时解析所有参数处理逻辑,而不是运行时。

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
T adder(T v) {
return v;
}

template<typename T, typename... Args>
T adder(T first, Args... args) {
return first + adder(args...);
}

// Usage: long sum = adder(1, 2, 3, 8, 7);

上面的adder函数可以接受任意数量的参数,并且只要+运算符能够应用于这些参数,就可以正常编译,其中的检查是编译器完成的,遵循的是模板语法和重载规则。

typename... Args是模板参数包,而Args... args是函数参数包,而该模板的编写方式与编写递归代码一样,需要一个基本的、接受一个参数的adder函数。每次调用函数的时候,都会从模板参数包中剥离一个类型T,缩短一个参数,直到遇到第一个函数模板。调用过程如下:

1
2
3
4
5
T adder(T, Args...) [T = int, Args = <int, int, int, int>]
T adder(T, Args...) [T = int, Args = <int, int, int>]
T adder(T, Args...) [T = int, Args = <int, int>]
T adder(T, Args...) [T = int, Args = <int>]
T adder(T) [T = int]

Some simple variations

C++模版元编程中有一个模式匹配的概念,以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
bool pair_comparer(T a, T b) {
// In real-world code, we wouldn't compare floating point values like
// this. It would make sense to specialize this function for floating
// point types to use approximate comparison.
return a == b;
}

template<typename T, typename... Args>
bool pair_comparer(T a, T b, Args... args) {
return a == b && pair_comparer(args...);
}

上面的函数接受任意数量的参数,如果参数成对相等,那么最终返回true,如下:

1
pair_comparer(1.5, 1.5, 2, 2, 6, 6)

但如果我们将第一个参数改成1,那么将会编译报错。同理,如果参数个数不为偶数,编译也不会通过。

1
2
pair_comparer(1.5, 1.5, 2, 2, 6, 6, 7);
pair_comparer(1, 1.5, 2, 2, 6, 6);

如果想避免这个问题,我们可以添加一个单参数的模版函数,这样就可以避免奇数参数编译不通过:

1
2
3
4
template<typename T>
bool pair_comparer(T a) {
return false;
}

Performance

关于性能的考虑,可变参数模版并没有涉及真正的递归,而是在编译时预生成一系列函数调用,而且由于现代编译器会对代码进行内联优化,很可能最后编译的机器代码中并没有函数调用。与C风格的可变参数函数相比,va_宏实际上是在操纵运行时堆栈,在运行时解析C语言的可变参数。

Varidic data structures

这个案例就比较复杂了,在C++11之前要实现具有动态添加新字段的自定义数据结构,是比较困难的。以下面的代码为例,我们进行类型定义:

1
2
3
4
5
6
7
8
template <class... Ts> struct tuple {};

template <class T, class... Ts>
struct tuple<T, Ts...> : tuple<Ts...> {
tuple(T t, Ts... ts) : tuple<Ts...>(ts...), tail(t) {}

T tail;
};

我们先从基类开始,定义了一个空的tuple类模版,后面的特化则从参数包中剥离出第一个类型,以此定义了一个名为tail的成员。通过递归定义,当没有更多类型可以剥离时就停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tuple<double, uint64_t, const char*> t1(12.2, 42, "big");

/*
struct tuple<double, uint64_t, const char*> : tuple<uint64_t, const char*> {
double tail;
}

struct tuple<uint64_t, const char*> : tuple<const char*> {
uint64_t tail;
}

struct tuple<const char*> : tuple {
const char* tail;
}

struct tuple {
}
*/

上面对于数据结构的定义使我们创建了tuple,其数据结构的大小和成员的内部布局都是确定的。另外,要想访问元祖,我们应该使用get函数模板来访问,定义一个帮助器类型,它允许我们访问元组中第k个元素的类型:

1
2
3
4
5
6
7
8
9
10
11
template <size_t, class> struct elem_type_holder;

template <class T, class... Ts>
struct elem_type_holder<0, tuple<T, Ts...>> {
typedef T type;
};

template <size_t k, class T, class... Ts>
struct elem_type_holder<k, tuple<T, Ts...>> {
typedef typename elem_type_holder<k - 1, tuple<Ts...>>::type type;
};

elem_type_holder是另一个可变参数类模板。它需要一个数字k和元组类型作为模板参数。这是一个编译时模板元编程,作用于常量和类型:

1
2
3
4
5
6
7
8
9
10
11
struct elem_type_holder<2, tuple<T, Ts...>> {
typedef typename elem_type_holder<1, tuple<Ts...>>::type type;
}

struct elem_type_holder<1, tuple<T, Ts...>> {
typedef typename elem_type_holder<0, tuple<Ts...>>::type type;
}

struct elem_type_holder<0, tuple<T, Ts...>> {
typedef T type;
}

以elem_type_holder <2,some_tuple_type>为例,其从tuple的开头剥离了两种类型,并将其类型设置为第三种类型。接下来再实现get。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <size_t k, class... Ts>
typename std::enable_if<k == 0, typename elem_type_holder<0, tuple<Ts...>>::type&>::type
get(tuple<Ts...>& t) {
return t.tail;
}

template <size_t k, class T, class... Ts>
typename std::enable_if<k != 0, typename elem_type_holder<k, tuple<T, Ts...>>::type&>::type
get(tuple<T, Ts...>& t) {
tuple<Ts...>& base = t;
return get<k - 1>(base);
}

/*
tuple<double, uint64_t, const char*> t1(12.2, 42, "big");

std::cout << "0th elem is " << get<0>(t1) << "\n";
std::cout << "1th elem is " << get<1>(t1) << "\n";
std::cout << "2th elem is " << get<2>(t1) << "\n";
*/

Variadic templates for catch-all functions

假设我们想要实现一个可以打印出标准库容器的函数,并且适用于任何容器。对于vector,list,deque来说,他们的摹本参数只有两个:value type和allocator type。但对于map和set来说,它们的参数个数都不止两个。因此我们可以使用可变模板来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <template <typename, typename...> class ContainerType,
typename ValueType, typename... Args>
void print_container(const ContainerType<ValueType, Args...>& c) {
for (const auto& v : c) {
std::cout << v << ' ';
}
std::cout << '\n';
}

template <typename T, typename U>
std::ostream& operator<<(std::ostream& out, const std::pair<T, U>& p) {
out << "[" << p.first << ", " << p.second << "]";
return out;
}