【C++ 模板元编程入门】在编译期实现 Peano 数

Like
Like Love Haha Wow Sad Angry
4

基本知识

类型的函数

我们都知道模板可以接受类型作为「参数」。同样地我们也可以有「返回值」,从而构造类型的函数。基本的范式是:

template<class T>
struct Computed {
  using type = T;
}

这就构造了一个名为 Computed 的,接收一个类型参数,返回这个类型本身的函数,用法如 Computed<float>::type,这个类型应当还是 `float`。
为什么要包一层 struct?这是因为 C++ 不支持对 using 的特化。这样的代码是不行的:

template <class T>
using computed<T> = T;

template<>
using computed<int> = double;

至于为什么不支持,我没有了解。

特化

什么是特化?你可以理解为模式匹配,就像 Haskell 中的写法一样。

template<class T>
struct Computed {
  using type = T;
}

template<>
struct Computed<int> {
  using type = double;
}

这样当你调用 Computed<bool>::type 时,得到的结果是 bool,而调用Computed<int>::type得到的结果却是double。当然这种匹配是遵循一定规则的,比如更「具体」的特化优先匹配,这跟 Haskell 谁在前谁先试着匹配不太一样。在 Haskell 中就好比:

data Type = Bool | Double | Int

computed :: Type -> Type
computed Int = Double
computed t = t

实际上有了这层对应,如果你知道怎么在 Haskell 中实现 Peano 数,那么 C++ 中的实现基本就是无脑翻译了。如果你不知道怎么在 Haskell 中实现 Peano 数,那你知道 Peano 数是什么,也能大差不差知道答案了。

Peano 数

Peano 数是什么?Peano 数是归纳定义的自然数,准确地说应该是一个表现形如直觉中「自然数」的公理系统,也就是「自然数」的形式化。这个系统里只有两个符号,Zero——表示 0,以及 Succ——表示后继。那么 1 就是 Succ<Zero>2 就是 Succ<Succ<Zero>>,以此类推(归纳,其实就是「以此类推」的形式化)。
我们可以在 C++ 中如此表述:

struct Peano {};
struct Zero: Peano {};
template<class T>
struct Succ: Peano {};

那么加法又是什么呢?从例子出发,我们需要定义一个两个类型参数的模板:

template<class T1, class T2>
struct Add {
  using type = ???;
}

满足直觉中的运算规律,比如 2+1=3,翻译成 C++ 就是 Add<Succ<Succ<Zero>>, Succ<Zero>>::type = Succ<Succ<Succ<Zero>>>。当然类型之间没有等于号,准确地说应该用 std::is_same<T1, T2>,这其实也是通过特化实现的,比如(示意,非官方实现):

template<class T, class U>
struct is_same {
  static constexpr bool value = false;
};
 
template<class T>
struct is_same<T, T> {
  static constexpr bool value = true;
};

那么如何定义加法呢?对于有限的元素,我们当然可以为每一个实例做特化,比如:

template<>
struct Add<Succ<Succ<Zero>>, Succ<Zero>> {
  using type = Succ<Succ<Succ<Zero>>>;
}

也就是打表。C++ 编译器的模板深度一般都是有限的,所以这理论上是可以在实际操作中覆盖所有用例的。但是这明显太傻了。其实加法的定义只需要两条规则就可以覆盖:0 + b = b, (Succ a) + b = Succ (a + b)。翻译成 C++ 就是:

template<class T1, class T2>
struct Add;

template<class T>
struct Add<Zero, T> {
  using type = T;
};

template<class T1, class T2>
struct Add<Succ<T1>, T2> {
  using type = Succ<typename Add<T1, T2>::type>
};

注意那个 typename,gcc 并不知道后面那个 ::type 成员是类型还是变量,所以需要 typename关键字的提示。
这就算写完了,你可以测试看看,是不是满足 std::is_same<Add<Succ<Succ<Zero>>, Succ<Zero>>::type, Succ<Succ<Succ<Zero>>>>::value == true(需要 <type_traits> 头文件,或者上面自己写的那个模板(那就不用加 std::)。这么嵌套着写 Succ 太繁琐了,也不方便看,你可以简单地写一个模板来从整数生成类型:

template<int v>
struct peano {
  using type = Succ<typename peano<v - 1>::type>;
};

template<>
struct peano<0> {
  using type = Zero;
};

然后就可以去验证 Add<<peano<2>::type, peano<1>::type>::type 是不是等于 peano<3>::type 了。
至于加减乘除的其他运算,比较啊奇偶性啊其他的函数,只要你懂得了加法,恐怕就不难了。

练习:

Peano numbers | Codewars 完成加减乘除、奇偶性和比较大小的撰写,并通过测试。

广告时间:

Codewars.com 是一个很好的综合性、游戏化 OJ,除了算法(多是入门级的)之外,考察语言特性(较为深入)是其一大亮点,同时有很多 Haskell 方面的内容,包括我们喜闻乐见的读论文然后完形填空。题图是 Codewars 上上述练习的界面截图。

后记

当然我们还可以进一步「证明」我们印象中的结论,比如加法是满足交换律的,加法和乘法是满足分配率的,等等。这就是后话了。

Like
Like Love Haha Wow Sad Angry
4

记孙正威老师

Like
Like Love Haha Wow Sad Angry
622

今天是教师节,我研究了一夜 OCaml,早上六点才睡。醒来已是下午两点。对这个节日,向来没有什么感受,可能是我在学校比较独立,和老师交流较少。师者,所以传道授业解惑,但是老师既没传给我道,也没怎么授过业,我更没有仰仗老师解惑。也从小到大,我都是在孤军奋战。这可能也是我学习不大好的重要因素之一。

但是今天水着工信群,突然想起了些什么。我一个学数学的,为什么会在工信群呢?为什么我会对编程有执念呢?

虽然我进大学的时候,确实是在几乎不会编程的状态,但我之前确实学过编程。它甚至是我开始拥有自己电脑的契机。那是在五年级升六年级的暑假开始前不久(2005 年)。有一个叫孙正威的信息学老师选了 6 个人去学习“信息学竞赛”(没有人知道怎么选的,只知道被选中了)。那时候我连个人电脑都没有,唯一能接触电脑的机会是去姑姑家,而唯一做的事情就是玩 PowerPoint 的剪贴画,然后用彩色打印机打出来。这样的我当然不知道「信息学竞赛」是什么东西。

得知这个消息,是下午快放学的时候(或者在那之前)。并且我们被叫去了他在新教学楼 6 楼的办公室。向来不喜欢和老师有过多牵扯(因为我老是害怕是不是我犯什么事了)的我,被通知的那一刻还有些慌乱,因为觉得很麻烦。我当时还不认识这个老师,不过「孙正威」这个名字倒是见过不少次,因为机房里很多文件上都署着。这倒还在其次,最主要的是,虽然不认识,但是见过几次,印象是这个老师颇为严厉,经常训斥学生 。当然最终还是得去,没有理由逃掉。

那个办公室是内外两间,外间有一台电脑,装的是 98 还是 XP 我不记得了,又或者是 2000。似乎没有什么准备地,课程就这么开始了。先是导论,教学方式颇为朴素。有一本官方教材,老师按照教材的顺序给我们过内容,先开始就是什么是编程、什么是 QBASIC、如何使用 QBASIC 的 IDE 云云。因为有电脑的缘故,倒是可以实时做一些演示。虽说是很朴素,但只有 6 个学生,教学效果很好,而且内容全新,第一次让我在学校有了被教学的感觉。

导论课没多久,之后我们必须接触真正的算法和程序了。我们要到机房去练习。机房也在 6 楼,就在办公室门外走道的尽头。当然在机房里不仅有练习,也有《上古神器》Flash 小游戏和金山打字通(或者是另外的打字练习软件)。经过了不懈的练习,我把按顺序按 A-Z 的速度提高到了 500 个字母每分钟(后来就没涨过了,现在打字也是 500 kpm)。

有的时候我们不在老师办公室上课,而是找空教室。老师就坐在桌子上,十分随意,我们围坐在桌子上,可以自由走动,从一个桌子爬上另一个桌子。虽然经过观察,孙老师为人确实很严厉,但是课堂氛围也着实很随意。或许这就是小班化教学的最大好处吧。我从前没有享受过,后来也没有。

我对待学习一向是一丝不苟的,课内是这样,课外也是这样。当然更多是一种「义务」范围内的一丝不苟,比如作业认真做,考试好好考,但是从来不会去自己不喜欢的科目做额外的事情,比如找练习题来做,或者探究答题的原理。虽然在考语文的时候我总是对如何答题一无所知,但一到有空的时候,我还是不会想起去钻研怎么答题。当时对信息学竞赛,并不算得很有兴趣,但是我想我要做好。为了课后的练习,我软磨硬泡父母给我买了一台电脑,是当时在电视上做广告的「四千八百八十八,笔记本电脑抱回家」的神舟笔记本。

虽然有了电脑就不免玩游戏,但我也花了相当的时间摆弄 QBASIC。我始终记得有一次和我爸去打乒乓球,我打累了就在球馆的吧台上研究编程,好象是在写一个一百多行的程序。

除此之外,我还摆弄过些别的。阴差阳错地,我在书店买了一本关于「ACCESS」的书,然后就照做了起来。对着它的图形化窗体设计器,我最终完成了一个保险单管理系统(我妈是做保险的)。我把这个系统展示给(信息竞赛的)老师和同学看,得到了特别好评,被老师树立为「钻研」的典型。虽然这可能不是我在这个「班」里表现好的唯一地方。

暑假前,暑假中,课是照样地上。平时有其他课,只能放学后抽时间开课。暑假里时间多了些。即使是中考放假都没有阻断我们的课。我们混进学校的时候,还被家长当成是参加中考的小学生呢。此外还有一次到滨海还是哪里,去找另外的老师教。那个老师上来就问高精度除法,结果最后只有我一个人答上来。看起来其他同学对这个事情不怎么上心。

最后要比赛了,它是一个夏令营的形式。在盐城一小南小区办的。有 3 个名额,我在列。整个夏令营下来,我认识到了南京、常州还有大丰在这项事业上的深厚实力。考试很神奇,是在纸上写运行结果的(4 个题,每个题 10 个点),(回想起来很神奇,为什么会这样。难道是 QBASIC 的基础设施太不完善了?可是基于 stdio 的判题,总归是不依赖语言,只依赖可执行文件的吧?)我骗了一些分,最后考了 180/400,二等奖,说是差一点点就一等奖了。另外两个同学考了 10 分和 0 分。

这就是我上大学之前关于编程最好的记忆。第二年临近比赛,我被告知今年我们县不参加了,因为孙正威老师考上了公务员,不当小学老师了。实际上对孙正威老师,至今我都知之甚少。他怎么知道这个比赛的?他又怎么会这个比赛的?他是自学的吗?他也是现学现卖的吗?但他应当是一位很有能力的人,一位知道自己想做什么、也会去做的人。

虽然只有短短的两三个月,但回想起来,我总觉得有很久很久。为什么会印象这么深呢?为什么会感觉这么好呢?是因为老师教得好吗?还是因为只有 6 个学生,体会更深?又或者是因为这真的是全新的内容,不仅不是我预习过的,而且完全超出了我的认知范围?或是兼而有之呢?

在我平淡的中小学生活里,如此这般轰轰烈烈的事情,仅此一次。其后,这段记忆就被封印起来。每天是无聊的普通课程,从初中到高中。每天早上上学的时候,我都会仰望天空。我会不停地问自己:「这样的苦日子什么时候会结束呢?」

到大学以后,通过各种渠道,见识到了各种各样的人,我才知道有老师是多么的好,没有老师是多么的不好。但是老师岂是想有就能有的呢?这也是需要机缘的呀。我曾经是不怎么善于和人交流,也不怎么愿意和人交流。但这到底是因是果呢?

其后我又自学过编程,但是效果不怎么好。尤其是在卡在一本叫多少天入门 Visual Basic .NET 2005 的书上——实际上直到近些年我才成功装上 Visual Studio 2005。IDE 都没装上,实践就更都无从谈起了。我当时也不理解什么 eventArgs,什么回调函数。只是经常无聊地翻着。后来我的兴趣点基本完全主要在数学上。

虽然我可以自称是一个 power user,但着实对编程不怎么在行。大一大二的时候,我发觉自己迫切地需要一个在网页上渲染数学公式的扩展,但市面上的都很死,迫不得已就学着开发了一个 Chrome 插件,支持对任意域名的匹配(概念可能来自于 Stylish),还了解到回调函数、异步等概念。大三的时候,我又凭借这次的经验混进了求是潮技术研发中心。

在那里,我认识了很多可爱的人,比如森森,比如好厉害,比如大爷,比如叶子哥哥。我经常围观他们操作电脑,或者和他们做奇怪的探险。我从他们那里学到了 Chrome 开发者工具的高级使用,学到了 Linux 的高级使用,学到了一些信息安全操作。在我看来,这是才是理想的师生关系。我就像是他们的学徒,他们都是我的老师,他们把我引进了软件开发的大门。

qlbf 也是我的老师,他向我介绍了 Codewars 的,帮我入门了 parser。

现在我比以前不菜一些了,虽然还是很菜。

有时候我会想,我为什么这么菜呢?如果当时有很好的老师,我会不会好一点呢?如果我不是这么晚才遇见了这些可爱的老师,会不会好一点呢?可是,没有如果。好的老师,哪里是想有就有的呢?世间万事,哪里都能如意呢?分明都是机缘巧合。

尽管如此,我还是希望事情能好一些。所以我搞了编程团,搞了很多其他群,我还想搞社区(虽然拖了两年了,但那时确实太菜了)。也许是出于补偿心理或者感同身受,我总是好为人师,热衷于传播我知道的各种东西。因为我觉得这样是有帮助的,是对的。我不想别人也有我这种痛苦、失落而遗憾的经历。

而这一切的一切的起点,我对编程的执念、我对建群的执念,这些执念的缘起,就是 2005 年暑假前后,我们被孙正威老师讲授小学信息学竞赛的经历。

这就是我今天想说的。

Like
Like Love Haha Wow Sad Angry
622