递归之美 - Loki库TypeList源码剖析

2024-01-17 23:58

本文主要是介绍递归之美 - Loki库TypeList源码剖析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

递归之美 LokiTypeList源码剖析

 

TypeList概观

提起List,想必大家都不会陌生,它是一个元素的集合,并且提供了一些对该集合进行操作的方法,比如:计算集合中元素的个数、向集合中添加元素、获取给定索引处的元素等。我们所熟知的List中的元素一般都是实例化后的值,相关的操作也都是在运行期间进行的。本文将要剖析的List和上述的List在某种意义上比较相象,不过它所包含的元素都是类型(type),相关的操作是在编译期间进行的。

本文将要讲述的TypeList取自Andrei Alexandrescu的力作《Modern C++ Design》一书相关的Loki库,关于《Modern C++ Design》,C++的爱好者想必不会陌生,在该书中,作者向我们展现了C++设计的全新思维,把C++的表达能力发挥到了极至,而Loki库正是这种思维的具体表现。TypeLsitLoki库中最为基础、核心的组件,理解了TypeList就具备了观赏这道C++新景观的基础。

下面我们先来看看如何定义一个TypeList,这样可以对于TypeList先有一个感性的认识。

typedef TYPELIST_3(char, int, long) MyTypeList;

定义了一个具有三个元素的TypeLsit,这三个元素分别为:charintlongTYPELIST_3Loki库中提供的用于定义TypeList的工具,我们会在本文的后面进行介绍。

::Loki::Length ::value;

计算MyTypeList中元素的个数,结果为3

typedef ::Loki::TypeAt ::Result MyType;

获取MyTypeList中第1个元素(从0开始),此时MyType就是int

typedef ::Loki::Append ::Result MyTypeList1;

MyTypeList中在添加一个元素:float,结果为MyTypeList1。此时::Length ::value 等于4

好了,先介绍这么多,下面我们将介绍TypeList实现的一些相关的背景知识,包括:递归的基本概念、模板的特化(tempalte specilization)、模板的偏特化(template partial specilization)以及类型萃取技术(type traits)。

TypeList相关技术

递归概述

对于递归,大家肯定都不陌生,使用递归方法给出的解决方案总是显得非常的优雅、简洁。不过递归方法所适合解决的问题应该符合下面的条件:

    1. 一个问题的解决依赖于一个较小规模的同样的问题的解决

    2. 必须有一个明确的结束条件

    3. 这个结束条件是可达的

如果一个问题符合上述的三个条件,我们就可以使用递归的方法。首先我们定义一套解决问题的规则,接着缩小问题的规模并应用同样的规则直到达到结束条件,然后结果层层返回直到原始问题。著名的汉诺塔问题就是一个典型的递归问题,如果不使用递归方法,解决汉诺塔问题就会显得非常的复杂,晦涩。

我们在使用递归方法设计程序时,这个递归过程的调用总是在运行期间进行的。本文所介绍的TypeList的实现中,递归的执行是在编译期间进行的,那么在编译期间如何定义每次递归的返回结果,如何定义结束条件呢?其中主要使用了下面将要介绍的模板特化、偏特化以及类型萃取技术。

模板特化和偏特化(template specilizaitonpartial specilization

什么是模板的特化、偏特化呢?大致的意思为:如果一个template拥有一个或者一个以上的template参数,我们可以针对其中一个或者多个参数进行特化处理(如果全部进行特化处理就是全特化,否则就是偏特化,切记:函数模板只能进行全特化,不能进行部分特化)。也就是说,我们可以提供一个特别版本,符合泛化条件,但是其中某些(全部)template参数已经由实际类型或者数值取代。

假设我们有一个class template定义如下:

template

class C { … };

对于模板的偏特化,刚刚接触可能会存在一些误解:以为模板的偏特化版本一定是对template参数U或者V或者T(或者它们的任意组合)指定某个参数值。其实不是这样的,所谓模板的偏特化是指另外提供一份template的定义,它的具体含义可以和通用的template定义版本无关。在一个偏特化版本中,template 参数的个数并不需要吻合通用的 template 中的个数。然而,出现於于class 名称之后的参数个数必须吻合通用的 template 的参数个数。下面举一个简单的例子进行说明:

template

class C { … };

这个泛型版本允许T为任意的类型。它的一个偏特化版本如下:

template

class C { … };

这个偏特化版本仅适用于T为原生指针类型的情况。

当我们使用C 去定义一个变量时,编译器会自动使用泛型版本,如果使用C 去定义一个变量时,编译器就会自动使用偏特化的版本。有了这个利器,我们就可以解决在编译期间定义递归的结束条件的问题。

类型萃取(type traits

类型萃取技术是泛型程序设计中的一个常用技术,它的思维核心为:把一系列与类型相关的性质包裹于单一的class 之内,这样我们就可以在编译期间获取一些所需要的和该类型相关的东西。其实这个思路就是软件领域一句著名的谚语:“任何事情都可以通过添加额外的中间层次得以解决”的又一次体现。通过把一系列想得到的类型相关的信息封装在另外一个类型定义中,这样就可以以一致的方式来对这些类型进行处理,提供了强大的可复用性和灵活性。

类型萃取技术一般都和模板的特化、偏特化技术结合在一起运用,这样它们就可以互相补充发挥出巨大的威力。下面简单举一个例子来了解一下类型萃取技术。

我们来看看Boost库中一个简单的template class is_pointer 的实现。我们需要一个主版本,用来处理T不为指针的所有情况,以及一个偏特化版本,用来处理T是指针的情况:

template

struct is_pointer

{ static const bool value = false; };

template

struct is_pointer

{ static const bool value = true; };

一个简单的示例如下:

template

void Func(T param)

{

if (is_pointer ::value) {

// do something

}

else {

//do something

}

}

通过类型萃取技术,我们就可以在编译期间保留每次递归的结果,供递归返回时使用。

关于这些技术的更多、更深入的介绍,请读者自行参考相关资料,不在此赘述。在下面的源码剖析中,读者将会看到这些技术的实际运用。

TypeList实现剖析

有了上述的背景知识,下面我们就来揭开TypeList的神秘面纱,走进TypeList的源码世界。首先,我们来看一下TypeList的定义。

TypeList定义

为了能够一致的进行TypeList的操作,在Loki库中定义了一个空类型NullType来标记一个TypeList的结束,NullTypeTypeList的定义如下:

class NullType { };

template

struct Typelist

{

typedef T Head;

typedef U Tail;

};

对于规范型TypeList的定义采用了一种尾递归的方法:

    1. NullType是规范的TypeLsit

    2. 如果T是规范的TypeList,那么对于任意原子类型UTypeListT>是规范的TypeList

Loki库中所采用的TypeList均为规范型的TypeList,这样可以在不减少灵活性的前提下简化对于TypeList的操作。本文后面所指的TypeList均为规范型的。

如何定义一个TypeList呢?比如:包含:charintlong三个元素的TypeLsit。根据上面的定义,可以得到如下的定义形式:

TypeList > >; // 注意两个>间一定要加一个空格,不

// 然编译器会认为是 >> 操作符

这样的定义方法显得比较麻烦、罗嗦,为了简化用户对于TypeList的使用,Loki库中采用了宏定义的方式对于大小在150TypeList进行了预定义:

#define TYPELIST_1(T1) ::Loki::Typelist

#define TYPELIST_2(T1, T2) ::Loki::Typelist

#define TYPELIST_3(T1, T2, T3) ::Loki::Typelist

依此类推。

这样用户在使用起来就会比较方便一些。

TypeList典型操作实现

了解了TypeList的定义,这里我们将对于TypeList相关的三个典型操作(LengthTypeAtAppend)的实现进行详细的剖析。掌握了这几个典型的操作,再学习其他的操作就会变得非常的容易。

我们将通过一个实例进行讲解,来看一下编译器实际的运作过程。我们定义了一个包含两个元素:int以及longTypeList

typedef TYPELIST_2(int, long) MyTypeList;

此时,编译器会根据TypeList的定义方式产生如下的类型定义结果:

struct TypeList

{

typedef long H;

typedef NullType T;

};

struct TypeList >

{

typedef int H;

typedef TypeList T;

};

 

Length的实现 获取TypeList中的元素个数:

template struct Length; // 仅有声明,没有实现,如果所传入的类型不是TypeLsit

// 的话,会产生一个编译期错误

template <> struct Length // 递归调用的结束条件,NullType的大小为0,运用了

{ // 模板特化和类型萃取技

//

enum { value = 0 };

};

template // 递归的规则定义,运用了模板偏特化和类型萃取技术

struct Length< Typelist >

{

enum { value = 1 + Length::value };

};

 

当通过Length ::value 获得MyTypeList中的元素个数时,看看编译器是如何根据我们指定的规则进行递归调用的。首先编译器会生成如下几个版本的Length定以:

struct Length >

{

enum { value = 1 + Length ::value };

};

struct Length > >

{

enum { value = 1 + Length >::value };

};

根据Length结束条件的定义可知,Length ::value 等于0,所以Length >::value 就等于Length ::value+1 ,也就是1。通过递推可知,Length ::value 也就是Length > >::value 等于Length >::value+1 ,也就是2。在层层的递推过程中,类型萃取技术得到了充分的体现,value就是我们想要得到的TypeList类型相关的信息,在每一层的递归过程中,都是通过它来保留结果的。

TypeAT的实现 获取给定位置处的元素

template struct TypeAt; // 仅有声明,没有实现,如果所传入

// 的类型不是TypeLsit

// 的话,会产生一个编译期错误

template

struct TypeAt , 0> // 递归调用的结束条件,如果给定位置为0,则

{ // 返回TypeList中的第一个元素

typedef Head Result;

};

template // 递归规则定义,注意这里的返回结果为类型,

struct TypeAt , i> // 运用了类型萃取技术。typename关键字的作

{ // 用是告诉编译器其后的实体是类型。

typedef typename TypeAt ::Result Result;

};

下面看看使用TypeAt ::Result 时,编译器都产生了那些动作。首先编译器要根据递归规则生成如下的类型定义:

struct TypeAt , 0>

{

typedef long Result;

};

struct TypeAt > , 1>

{

typedef TypeAt , 0>::Result Result;

};

很明显typedef TypeAt , 0>::Result Result; 就是typedef long Result;所以,TypeAt ::Result 就是long类型。同样的,在这个实现中充分使用了类型萃取技术,不过,这里我们想要的不是value,而是Result

 

Append的实现 TypeList的末尾添加一个元素

template struct Append; // 仅有声明,没有实现,如果所传入

// 的类型不是TypeLsit

// 的话,会产生一个编译期错误

template <> struct Append // 递归结束条件定义,模板的偏特化

{

typedef NullType Result;

};

template struct Append // 递归结束条件定义,模板的偏特化

{

typedef TYPELIST_1(T) Result;

};

template

struct Append > // 递归结束条件定义,模板的偏特化

{

typedef Typelist Result;

};

template // 递归规则定义,注意这里的返回结果为类型,

struct Append , T> // 运用了类型萃取技术。typename关键字的作

{ // 用是告诉编译器其后的实体是类型。模板的偏特

//

typedef Typelist

typename Append ::Result>

Result;

};

同样的,让我们来看看当使用Append ::Result 时,编译器的递归执行动作。首先看看编译器会生成的一些类型定义:

struct Append

{

typedef TYPELIST_1(float) Result;

};

struct Append , float>

{

typedef TypeList ::Result > Result;

};

struct Append >, float>

{

typedef TypeList ,float >::Result > Result;

};

经过简单的替换,Append ::Result 就等于:

typedef TypeList ,float >::Result >;

等于:

typedef TypeList ::Result > >;

等于:

typedef TypeList > >;

也就是:

TYPELIST_3(int, long, float) ;等同于在原有TypeList的末尾添加了一个新元素float。不用多说了,这里类型萃取技术同样发挥了巨大的作用。

 

结束语

本文对于TypeLsit的实现进行了剖析,相信读者朋友对于TypeList含义以及实现手法已经有所掌握。那么TypeLsit到底有什么用处呢?源码面前,了无秘密,掌握了TypeList,就掌握了全面理解Loki库的钥匙。在Loki库中,你将会看到Abstract Factory模式、Visitor模式这些泛型组件是如何在TypeLsit的基础上搭建起来的。背起你的行囊,拿起这把钥匙,赶快踏上你的“寻宝”之路吧(Loki库的源代码可以从www.moderncppdesign.com下载)。




这篇关于递归之美 - Loki库TypeList源码剖析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/617571

相关文章

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Jackson库进行JSON 序列化时遇到了无限递归(Infinite Recursion)的问题及解决方案

《Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursion)的问题及解决方案》使用Jackson库进行JSON序列化时遇到了无限递归(InfiniteRecursi... 目录解决方案‌1. 使用 @jsonIgnore 忽略一个方向的引用2. 使用 @JsonManagedR

Spring 中 BeanFactoryPostProcessor 的作用和示例源码分析

《Spring中BeanFactoryPostProcessor的作用和示例源码分析》Spring的BeanFactoryPostProcessor是容器初始化的扩展接口,允许在Bean实例化前... 目录一、概览1. 核心定位2. 核心功能详解3. 关键特性二、Spring 内置的 BeanFactory

Rust中的BoxT之堆上的数据与递归类型详解

《Rust中的BoxT之堆上的数据与递归类型详解》本文介绍了Rust中的BoxT类型,包括其在堆与栈之间的内存分配,性能优势,以及如何利用BoxT来实现递归类型和处理大小未知类型,通过BoxT,Rus... 目录1. Box<T> 的基础知识1.1 堆与栈的分工1.2 性能优势2.1 递归类型的问题2.2

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一