引言
我一直对C++
模板元编程的各种用法非常感兴趣,就想尝试手动实现std::tuple
这个类型。期间碰到了不少问题,但在Google和自己的努力之下,都得到了解决。我觉得通过对一些模板工具的实现,能够更加清楚地理解模板元编程。
前置知识
为了更好的理解这篇文章的内容,读者可能需要有以下前置知识
- 可变模板参数是什么
- 什么是模板类的特化和偏特化
- 多态的一些性质
std::tuple
的接口及用法
我们要实现自己的tuple
,可以先观察stl
的tuple
特点、用法和接口,接着可以开始设计我们自己的数据结构,一点一点将tuple
实现出来。
tuple
对象的声明及初始化
// 显式的声明及初始化
// 模板参数既可以是空、也可以是N多个
std::tuple<int, double, char> t1 {1, 1.0, 'a'};
// 调用make_tuple,声明、初始化
// t1 和 t2的类型是一致的
auto t2 = std::make_tuple(1, 1.0, 'a');
// 空模板参数的tuple
std::tuple<> t3;
// 4个模板参数的tuple
std::tuple<int, char, long, float> t4 {1, 'a', 5L, 3.14f};
tuple
对象的声明和初始化方式和特点可以总结为:
- 构造函数初始化
make_tuple
初始化tuple
可以存储任意多的参数数据
取值和赋值
auto t1 = std::make_tuple(1, 3.14, 'c');
// 取值,将3.14取出来,类型为double
// 输出:3.14
std::cout << std::get<1>(t1) << std::endl;
// 赋值
std::get<1>(t1) = 5.12;
// 输出:5.12
std::cout << std::get<1>(t1) << std::endl;
对于tuple对象的取值和赋值方法及特点,可以总结为:
- 只能通过
std::get
取值和赋值,如std::get<2>
可以将tuple
中第2个(从0开始)参数取出来 std::get
可以对tuple
的参数进行赋值,可以看出,std::get
返回的是引用
从零开始——实现std::tuple
从这一节开始,我们就要设计我们自己的tuple
了~在代码中,我们的tuple
将被命名为tu
设计数据结构
我们的tu
需要存储任意多的参数,这就需要用到变长模板类。
// code1
template<typename... Ts>
struct tu;
注意:
- 在
C++
中,struct
和class
在内存结构上是一致的,都可以有构造函数、析构函数,成员函数和成员变量也有公有和私有之分;struct
和class
的唯一区别就是struct
的成员默认是public
,而class
默认是private
- 这里只声明并不实现
tu
也可以不储存数据,也就是模板参数为空,我们来将空模板类tu
特化出来。
// code2
template<> struct tu<>{};
using null_tu = tu<>;
- 这里将tu特化实现出来
- 第二行
using
的用法相当于typedef
,这一行相当于:typedef tu<> null_tu;
。using
和yuanlitypedef
的区别就在于using
可以跟模板一起使用,请参考:https://stackoverflow.com/questions/10747810/what-is-the-difference-between-typedef-and-using-in-c11
下面使用递归的模板类,将变长参数Ts
的每一个类型取出来。这个类是tu
的偏特化。
// code3
template<typename T, typename ...Ts>
struct tu<T, Ts...> : tu<Ts...> {
T value;
};
这段代码是什么意思呢,举个栗子:
假如我们需要声明tu<int, char, double>
,根据第一段代码(code1
),code1
里的Ts=[int, char, double]
。但是因为code1
并没有具体的实现,这个时候就匹配到第三段的偏特化代码(code3
),相当于[int, char, double]
匹配到T, Ts...
上,也就是T=int
和Ts=[char, double]
。接着,tu<T, Ts...>
继承于tu<Ts...>
,这就是一个递归的继承了,我们把这个继承关系写清楚,就是struct tu<int, [char, double]> : tu<[char, double]>
(伪代码,中括号中表示变长模板参数Ts
),将递归展开就是struct tu<int, [char, double]> : tu<char, [double]> : tu<double> : tu<>
这段代码将所有需要的参数类型都存储进去了,接着我们就需要接着实现构造函数了。将code3
修改为:
// code4
template<typename T, typename ...Ts>
struct tu<T, Ts...> : tu<Ts...> {
tu<T, Ts...>()
: value(T()), tu<Ts...>() {}
explicit tu<T, Ts...>(const T &v, const Ts &...args)
: value(v), tu<Ts...>(args...) {}
T value;
};
先对value
进行初始化,再对基类进行初始化操作。这里赋值构造函数其实是不完整的,还是以上面的tu<int, char, double>
为例,tu<int, char, double>
会调用基类赋值构造函数tu<char, double>
,tu<char, double>
又会调用tu<char>
的赋值构造函数,这里也没问题的。但是tu<char>
这里会再调用tu<>
的赋值构造函数,然而tu<>
没有赋值构造函数也不需要复制构造函数。我们这里就需要将只有一个模板参数的tu
再偏特化一下,code4
需改为:
// code5
template<typename T, typename ...Ts>
struct tu<T, Ts...> : tu<Ts...> {
tu<T, Ts...>()
: value(T()), tu<Ts...>() {}
explicit tu<T, Ts...>(const T &v, const Ts &...args)
: value(v), tu<Ts...>(args...) {}
T value;
};
template<typename T>
struct tu<T> : null_tu {
tu<T>() : value(T()) {}
explicit tu<T>(const T &v) : value(v) {}
T value;
};
tu
的存取
tu
存取的原理
我们现在需要读取tu
的数据了,该怎么做呢?比如说tu<int, double, char> t { 1, 3.14, 'a' };
,继承关系是这样的:tu<int, double, char>
→tu<double, char>
→tu<char>
→tu<>
,除了最后一个tu<>
之外,每一层都有一个value
成员变量,tu<int, double, char>
的value
可以直接访问到,tu<double, char>
的value
可以通过强制转换成基类指针来访问。
// code6
tu<int, double, char> t { 1, 3.14, 'a' };
// 输出:a
std::cout << ((tu<char>*)&t)->value << std::endl;
// 输出:3.14
std::cout << ((tu<double, char>*)&t)->value << std::endl;
// 输出:1
std::cout << t.value << std::endl;
tu
的萃取器
上面这段代码就是访问数据的原理,有了原理,那么我们得知道些什么呢?1,必须知道要取值的类型;2,必须知道要取值的tu
的类型。我们下面就要设计一个萃取器来将需要取值的类型和tu
类型萃取出来。
首先,声明一个类g_impl
,这个类就是我们的萃取器。
// code7
template<std::size_t Index, typename... Ts>
struct g_impl;
同样,这个部分只声明不实现。
这个萃取器g_impl
我们希望实现两个功能:
- 将第
Index
个参数的类型取出来 - 将第
Index
个参数对应的类(基类)取出来
// code8
// 将第Index个参数的类型取出来
g_impl<Index, tu<Ts...> >::value_type
// 将第Index个参数对应的类(基类)取出来
g_impl<Index, tu<Ts...> >::tu_type
// 例如
using t = tu<int, double, char>;
// 将第一个类型取出来,就是double
using double_type = g_impl<1, t>::value_type;
// 将第二个类型对应的类(基类)取出来,就是tu<double, char>
using base_type1 = g_impl<1, t>::tu_type;
如何来实现呢?还是要用到递归。来看代码
// code9
template<std::size_t Index, typename T, typename ...Ts>
struct g_impl<Index, tu<T, Ts...> > {
// 递归推理
using value_type = typename g_impl<Index - 1, tu<Ts...> >::value_type;
using tu_type = typename g_impl<Index - 1, tu<Ts...> >::tu_type;
};
// 将递归的推理类型补充完整,Index=0的情况
template<typename T, typename ...Ts>
struct g_impl<0, tu<T, Ts...> > {
using value_type = T;
using tu_type = tu<T, Ts...>;
};
// 将递归的推理类型补充完整,tu<>的情况
template<>
struct g_impl<0, nulltu> {
using value_type = void;
using tu_type = nulltu;
};
下面我们还是以tu<int, double, char>
为例来解释,如果我们要萃取第1个参数的类型double
,写全了就是
// code10
// 伪代码,[double, char]表示可变模板参数 ...Ts
struct g_impl<1, tu<int, [double, char]> > {
using value_type = typename g_impl<0, tu<double, char> >::value_type;
};
我们再将typename
后面的g_impl<0, tu<double, char> >
补全:
// code11
// 伪代码,[char]表示可变模板参数 ...Ts
struct g_impl<0, tu<double, [char]> > {
// 当Index为0时,就不需要再递归了,就是当前的类型
using value_type = double;
}
对tu_type
的递归推倒也是一样的原理,大家可以自行推倒理解。
tu
的取值赋值函数
有了类型的萃取器,我们就能知道目标参数类型和参数对应的类(基类)。再结合上面取值的原理
template<std::size_t Index, typename ...Ts>
typename g_impl<Index, tu<Ts...> >::value_type& g(tu<Ts...> &t) {
using tu_t = typename g_impl<Index, tu<Ts...> >::tu_type;
return ((tu_t *)&t)->value;
}
g_impl
部分就是萃取器。注意,返回值是typename g_impl<Index, tu<Ts...> >::value_type&
,是个引用类型。使用引用返回值,就可以实现对tu
的数据的需改了。
完整代码
// 变长模板参数
// 只声明不实现
template<typename ...Ts>
struct tu;
// 空模板参数的特化
template<> struct tu<>{};
using nulltu = tu<>;
// 递归的类型
// 偏特化,相当于tu的“重载函数”
template<typename T, typename ...Ts>
struct tu<T, Ts...> : tu<Ts...> {
using base_type = tu<Ts...>;
using this_type = tu<T, Ts...>;
using type = T;
tu<T, Ts...>()
: value(T()), base_type() {}
explicit tu<T, Ts...>(const T &v, const Ts &...args)
: value(v), base_type(args...) {}
T value;
};
// 将递归的类型补充完整
template<typename T>
struct tu<T> : nulltu {
using base_type = nulltu;
using this_type = tu<T>;
using type = T;
tu<T>() : value(T()) {}
explicit tu<T>(const T &v) : value(v) {}
T value;
};
// 实现g<0>(tu)的声明
template<std::size_t Index, typename ...Ts>
struct g_impl;
// 相当于第二个参数传入tu的重载
// 将T分离出来
template<std::size_t Index, typename T, typename ...Ts>
struct g_impl<Index, tu<T, Ts...> > {
// 递归推理
using value_type = typename g_impl<Index - 1, tu<Ts...> >::value_type;
using tu_type = typename g_impl<Index - 1, tu<Ts...> >::tu_type;
};
// 将递归的推理类型补充完整,Index=0的情况
template<typename T, typename ...Ts>
struct g_impl<0, tu<T, Ts...> > {
using value_type = T;
using tu_type = tu<T, Ts...>;
};
// 将递归的推理类型补充完整,tu<>的情况
template<>
struct g_impl<0, nulltu> {
using value_type = nulltu;
using tu_type = nulltu;
};
// typename g_impl<Index, tu<Ts...> >::value_type& 为返回类型的推理
// tu_t为第Index个基类的类型
// 强制转换成基类的类型,并访问变量
template<std::size_t Index, typename ...Ts>
typename g_impl<Index, tu<Ts...> >::value_type& g(tu<Ts...> &t) {
using tu_t = typename g_impl<Index, tu<Ts...> >::tu_type;
return ((tu_t *)&t)->value;
}
// 实现make_tuple函数
template<typename ...Ts>
tu<Ts...> make_tu(const Ts &...args) {
return tu<Ts...>{args...};
}
总结
在搞清楚tuple
的原理之后,发现了几个特点:
- 可变模板参数都是通过递归来获取每个参数的类型
- 递归类型推倒的时候需要将所有递归类型的情况都想清楚,必须将类型补充完整
- 模板元编程大概就是SFINAE(substitution failure is not an error,匹配失败不是错误)吧(关于SFINAE是什么,请看https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error )
就写到这里吧~希望对看到文章的人能有所帮助~
2 comments
可怕,旭旭最近在徒手画canvas,你再徒手写tuple,而我在上班摸鱼看你博客
最近比较懒,都没再写了...