最近在重温《c++程序设计新思维》这本经典著作,感慨颇多。由于成书较早,书中很多元编程的例子使用c++98实现的。而如今c++20即将带着concept,Ranges等新特性一同到来,不得不说光阴荏苒。在c++11之后,得益于新标准很多元编程的复杂技巧能被简化了,STL也提供了诸如<type_traits>这样的基础设施,c++14更是大幅度扩展了编译期计算的适用面,这些都对元编程产生了不小的影响。今天我将使用书中最简单也就是最基础的元容器TypeList来初步介绍现代c++在元编程领域的魅力。
本文索引什么是TypeList
TypeList的定义
元函数的实现
Length元函数求list长度
TypeAt获取索引位置上的类型
IndexOf获得指定类型在list中的索引
Append为TypeList添加元素
Erase和EraseAll删除元素
NoDuplicates去除所有重复type
Replace和ReplaceAll
Derived2Front将派生类型移动至list前部
元函数实现总结
示例
自制tuple
简化工厂模式
总结
什么是TypeListTypeList顾名思义,是一个存储和操作type的list,你没有看错,存储的是type(类型信息)而不是data。
这些被存储的type也被称为元数据,存储的它们的TypeList也被称为元容器。
那么,我们存储了这些元数据有什么用呢?答案是用处很多,比如tuple,工厂模式,这两个后面会举例;还可用来实现CRTP技巧(一种元编程技巧),线性化继承结构等,这些也在原书中有详细的演示。
不过光看我在上面的解释多半是理解不了什么是TypeList以及它有什么用的,不过没关系,元编程本身就是高度抽象的脑力活动,只有多读代码勤思考才能有所收获。下面我就展示如何使用现代c++实现一个TypeList,以及对c++11以前的古典版本做些简单的对比。
TypeList的定义最初的问题是我们要如何存储类型呢?数据可以存变量,单是type和data的不同的东西,怎么办?
聪明的你可能以及想到了,我们可以让模板参数成为type信息的容器。
但是紧接着第二个问题来了,所谓list它的元素数量是固定的,但是直到c++11以前,模板参数的数量都是固定的,那么怎么办?
其实也很简单,参考普通list的链表实现法,我们也可以用相同的思想去构造一个“异质链表”:
template <typename T, typename U> struct TypeList { typedef T Head; typedef U Tail; };这就是最简单的定义,其中,T是一个普通的类型,而U则是一个普通类型或TypeList。创建TypeList是这样的:
// 创建unsigned char和signed char的list typedef TypeList<unsigned char, signed char> TypedChars; // 现在我们把char也添加进去 typedef TypeList<char, TypeList<unsigned char, signed char> > Chars; // 创建int,short,long,long long的list typedef TypeList<int, TypeList<short, TypeList<long, long long> > > Ints;可以看到,通过TypeList环环相扣,我们就能把所有的类型都存储在一个模板类组成的链表里了。但是这种实现的弊端有很多:
首先是定义类型不方便,上面的第三个例子中,仅仅为了4个元素的list我们就要写出大量的嵌套代码,可读性大打折扣;
原书中提到,为了简化定义,Loki库提供了TYPELIST_N这个宏,但是它是硬编码的,而且最大只支持50个元素,硬编码在程序员的世界里始终是丑陋的,更不用说还存在硬编码的数量上限,而且这么做也违反了“永远不要复读你自己”的原则,不过对于c++98来说只能如此;
我们没办法清晰得表示只有一个元素或是没有元素的list,所以我们只能引入一个空类NullType来表示list的某一位上没有数据存在,比如:TypeList<char, NullType>或TypeList<NullType, NullType>,当然,你特化出单参数的TypeList也只是换汤不换药。
无法有效得表示list的结尾,除非像上面一样使用NullType最为终结标志。
好在现代c++有变长模板,上述限制大多都不存在了:
template <typename...> struct TypeList; template <typename Head, typename... Tails> struct TypeList<Head, Tails...> { using head = Head; using tails = TypeList<Tails...>; }; // 针对空list的特化 template <> struct TypeList<> {};通过变长模板,我们可以轻松定义任意长度的list:
using NumericList = TypeList<short, unsigned short, int, unsigned int, long, unsigned long>;同时,我们特化出了空的TypeList,现在我们可以用它作为终止标记,而不用引入新的类型。如果你对变长模板不熟悉,可以搜索相关的资料,cnblogs上就有很多优质教程,介绍这个语法特性已经超过了本文的讨论范畴。