Loading... ## 引言 我一直对`C++`模板元编程的各种用法非常感兴趣,就想尝试手动实现`std::tuple`这个类型。期间碰到了不少问题,但在Google和自己的努力之下,都得到了解决。我觉得通过对一些模板工具的实现,能够更加清楚地理解模板元编程。 ### 前置知识 为了更好的理解这篇文章的内容,读者可能需要有以下前置知识 1. 可变模板参数是什么 2. 什么是模板类的特化和偏特化 3. 多态的一些性质 ## `std::tuple`的接口及用法 我们要实现自己的`tuple`,可以先观察`stl`的`tuple`特点、用法和接口,接着可以开始设计我们自己的数据结构,一点一点将`tuple`实现出来。 ### `tuple`对象的声明及初始化 ```cpp // 显式的声明及初始化 // 模板参数既可以是空、也可以是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`对象的声明和初始化方式和特点可以总结为: 1. 构造函数初始化 2. `make_tuple`初始化 3. `tuple`可以存储任意多的参数数据 ### 取值和赋值 ```cpp 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对象的取值和赋值方法及特点,可以总结为: 1. 只能通过`std::get`取值和赋值,如`std::get<2>`可以将`tuple`中第2个(从0开始)参数取出来 2. `std::get`可以对`tuple`的参数进行赋值,可以看出,`std::get`返回的是引用 ## 从零开始——实现`std::tuple` 从这一节开始,我们就要设计我们自己的`tuple`了~在代码中,我们的`tuple`将被命名为`tu` ### 设计数据结构 我们的`tu`需要存储任意多的参数,这就需要用到变长模板类。 ```cpp // code1 template<typename... Ts> struct tu; ``` > 注意: > 1. 在`C++`中,`struct`和`class`在内存结构上是一致的,都可以有构造函数、析构函数,成员函数和成员变量也有公有和私有之分;`struct`和`class`的唯一区别就是`struct`的成员默认是`public`,而`class`默认是`private` > 2. 这里只声明并不实现 `tu`也可以不储存数据,也就是模板参数为空,我们来将空模板类`tu`特化出来。 ```cpp // code2 template<> struct tu<>{}; using null_tu = tu<>; ``` > 1. 这里将tu特化实现出来 > 2. 第二行`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`的偏特化。 ```cpp // 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`修改为: ```cpp // 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`需改为: ```cpp // 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`可以通过强制转换成基类指针来访问。 ```cpp // 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`,这个类就是我们的萃取器。 ```cpp // code7 template<std::size_t Index, typename... Ts> struct g_impl; ``` 同样,这个部分只声明不实现。 这个萃取器`g_impl`我们希望实现两个功能: 1. 将第`Index`个参数的类型取出来 2. 将第`Index`个参数对应的类(基类)取出来 ```cpp // 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; ``` 如何来实现呢?还是要用到递归。来看代码 ```cpp // 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`,写全了就是 ```cpp // 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> >`补全: ```cpp // code11 // 伪代码,[char]表示可变模板参数 ...Ts struct g_impl<0, tu<double, [char]> > { // 当Index为0时,就不需要再递归了,就是当前的类型 using value_type = double; } ``` 对`tu_type`的递归推倒也是一样的原理,大家可以自行推倒理解。 #### `tu`的取值赋值函数 有了类型的萃取器,我们就能知道目标参数类型和参数对应的类(基类)。再结合上面取值的原理 ```cpp 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`的数据的需改了。 #### 完整代码 ```cpp // 变长模板参数 // 只声明不实现 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`的原理之后,发现了几个特点: 1. 可变模板参数都是通过递归来获取每个参数的类型 2. 递归类型推倒的时候需要将所有递归类型的情况都想清楚,必须将类型补充完整 3. 模板元编程大概就是SFINAE(substitution failure is not an error,匹配失败不是错误)吧(关于SFINAE是什么,请看https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error ) 就写到这里吧~希望对看到文章的人能有所帮助~ <img src="https://cdn.jsdelivr.net/gh/ihewro/handsome-static@8.2.0.2/assets/img/emotion/aru/cheer.png" class="emotion-aru"> Last modification:November 13, 2018 © Allow specification reprint Like If you think my article is useful to you, please feel free to appreciate
2 comments
可怕,旭旭最近在徒手画canvas,你再徒手写tuple,而我在上班摸鱼看你博客