以前很喜欢一本 C++ 的书叫做《Modern C++ Design: Generic Programming and Design Patterns Applied》,这是一本讲 C++ 模板编程的书,不过讲的东西比较偏实际一点,并不像其他 C++ 模板的书那么“重口味”……好吧,我承认,只要涉及到 C++ 模板编程的,重口味是在所难免的,不过总的来说我特别喜欢里面叫做 Policy-Based Design 的一章内容,我还记得我当时是在东四那个眼前一片青葱的大木桌那里看的。
Policy-based Programming 基本上算是用模板 + OO 的方式来进行代码共享。例如,我现在正在实现一种叫做 Conditional Probability Tree (Beygelzimer, Langford, Lifshits, Sorkin, & Strehl, 2009) 的树状 multiclass 分类器 。在一棵树中每一类对应一个叶子节点,该分类器动态地构建树结构,如果碰到一种在树中没有的新类别,则会创建一个新的叶子节点来对应该类。新的叶子节点的插入位置是从树根开始往下找,在每一个节点处根据一个策略决定是放在左边子树还是右边子树。这里暂时有两种策略:一是随机,二是根据当前得到的目标函数来做判断。在通常的 C++ 或者 OO 中(或者至少是在 shogun 的 dev-team 的习惯里),一般会用继承和重载来对这里进行处理。大概会像这个样子:
- class CPT
- {
- // ...
- protected:
- virtual bool which_subtree()=0;
- };
- class RandomCPT: public CPT
- {
- protected:
- virtual bool which_subtree()
- {
- if (rand() % 2 == 0)
- return true;
- return false;
- }
- };
- class BalancedCPT: public CPT
- {
- protected:
- virtual bool which_subtree()
- {
- // do some sophisticated calculating
- }
- };
在每个子类中重载 which_subtree
方法来实现不同的策略。这样的做法需要用到虚函数,我们都知道 C++
中虚函数是通过 virtual table
来实现的,调用的时候需要在运行时解析函数的地址,因此会比较慢。比较臭名昭著的例子就是
C 的标准库里的 qsort
,它接受一个函数指针来对数组的元素进行比较,由于函数指针的调用很慢,所以
qsort 也就出奇的慢,这个也许经常用 C 做 ACM
题的同学可能会比较有感触。最近我还发现 qsort
引发了一宗“冤假错案”:有一个比较抵制 STL 的朋友(似乎主要原因是他开始用
C++ 的时候 C++ 标准和 STL
本身都还处于相当不完善的状态,给人留下不好的印象吧),我问他为什么讨厌
STL 的时候,他说了一些理由,然后提到 STL
的实现各种有问题,我自己写了个排序的函数比标准库里的函数快了好几个数量级。我觉得很难以置信,后来仔细一问才发现原来他说的是
C 的 qsort 。:D
实际上,在 C++ 中 qsort 这个问题已经得到解决了。C
之所以会出现这种尴尬的情况是因为它除了函数指针之外没有办法传递一个东西可以用来自定义排序时候数组元素大小比较的行为。而
C++ 里多了一个叫做 functor 的东西。看一下 C++ 的 std::sort
的签名:
- template <class RandomAccessIterator, class Compare>
- void sort(RandomAccessIterator first, RandomAccessIterator last,
- Compare comp);
这里的 comp 参数就是一个 functor
(还有另外一个重载版本可以省略该参数,直接使用
operator < 进行比较)。一个 functor 其实就是一个普通的
C++ 对象,不过我们要求它定义了 operator ()
,并接受合适的参数以及有合适的返回值:比较可惜的是 C++ 11
之前都没有合适的语法来描述这些要求,甚至都没法事先指定 comp
是一个什么类型的对象——事实上它可以是任意类型,只要实现了合适的
operator ()
的语法,甚至可以就是一个普通的裸函数指针。所以这里只好把
comp 的类型纳入模板参数里。
缺乏语法来对 comp
的行为进行描述的后果就是:当代码符合要求的时候,一切都可以正常工作;但是如果代码有不符合的情况(例如
functor
的括号运算符对应的参数列表和要求的不匹配),就完蛋了:可能会出现各种稀奇古怪的编译错误提示,这可能算是
C++ 的模板的最大的缺点了吧(另一个巨大缺点是编译异常缓慢),特别是像 boost 这种 heavily
使用模板的库,用对了固然很爽,但是一旦出错了通常会非常崩溃,有时候看起来明明似乎是对的代码可是就是编译不过,而长达几千页的错误输出也只会帮倒忙而已。
好在 C++ 11 出来了许多新的语言特性用于解决这些问题,例如 static_assert 通常可以让许多诡异的模板错误变得更加 human-friendly ,而特地针对 functor ,也有许多改进。比如引入了一个可以定义 functor 的“类型”的东西,例如
std::function<int (int, int)> func;
指定 func 是一个接受两个 int 参数,返回一个
int 参数的函数指针或者 functor 或者 whatever
。这样语法也就明确了许多,签名不匹配的时候也能得到更友好的错误信息。
说了这么多最重要的一点查点忘了:functor 之所以比函数指针好,是因为
functor 的 operator ()
的地址是可以在编译期确定的(除非你很邪恶地用虚操作符然后还用 functor
引用来调用……),所以不需要再在运行的时候一遍一遍地计算地址,编译器和 CPU
也都能做更多的优化了:比如内联。这些东西如果用函数指针的话,是完全没法做的(除非再搞个强大的
JIT )。所以虽然 qsort 很慢,STL 里的
std::sort 应该还是很快的。
不过,如果仅仅因为调用效率问题就直接完全拒绝掉虚函数的话,又太武断了。因为除非是像
qsort
这种需要反复调用那个传进来的函数的情况,否则一般函数指针调用和普通函数调用之间的性能差异是感觉不出来的,这么做就完全犯了
premature optimization 的大忌了。
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Donald Knuth. Structured Programming with go to Statements, ACM Journal Computing Surveys, Vol 6, No. 4, Dec. 1974. p.268.
但是话说回来,像 which_subtree
这种函数应该也是会在训练模型的时候被调用不少次的,至少可以用一个
profiler 来分析一下。不过这里还有另一个重要的理由让我对于这种纯 OO
的设计有所疑虑。Conditional Probability Tree
作为一个树状的分类器,它其实是由许多子分类器组成的。具体来说,每一个节点都对应一个回归模型,用于估计走左边和走右边的概率。节点上使用的回归模型不妨称为
base regressor ,可选的 base regressor 可以有 LibLinear、Vowpal Wabbit
等等,但是由于历史原因以及各个算法本身的差异性等各种原因,这些各种类型的
base regressor
没有一个抽象得很好的基类可以供使用,所以我得针对不同的算法来定制一些操作,于是麻烦来了:如果继续使用继承和重载的方式的话,我就需要实现
RandomLibLinearCPT , RandomVwCPT ,
BalancedLibLinearCPT 和 BalancedVwCPT
等各种子类。其中有一些代码会不得不被复制两份,bad smell 出现了……
类似的情况经常在实际中发生:你需要从不同的角度来定制一个类的行为,这些不同的角度之间互相是正交的,所以如果简单地使用继承的方式来处理的话,需要实现的类的数量将会按照笛卡尔乘积的方式增长。这个时候 policy-based programming 就可以帮忙了。啥也不说,先上代码:
- template <class SubtreePolicy, class RegressorPolicy>
- class CPT: public SubtreePolicy, public RegressorPolicy
- {
- public:
- void train()
- {
- // ...
- if (SubtreePolicy::which_subtree() == true) {
- // go to left subtree
- } else {
- // go to right subtree
- }
- // ...
- m_regressors.push_back(RegressorPolicy::clone(...));
- }
- private:
- std::vector<typename RegressorPolicy::regressor_t> m_regressors;
- };
啊,其实是伪代码。这里我调用函数的时候故意把它属于哪个父类的
qualifier 给写出来了,只是为了方便展示。这里的所谓 policy 可以看作是
functor 的一种推广,它不再局限于 operator ()
,而是可以有各种成员函数(例如 which_subtree
)以及可以定义类型(例如 regressor_t
)等等所以东西。这里我们采用继承的方式来使用 policy
,这样可以很方便地使用 policy
里的成员(函数、类型等),当然也可以不使用继承,而将 policy object
作为成员变量保存起来使用也是没有任何问题的。
这样一来就很好地解决了我们刚才碰到的正交性的问题,现在我们可以分别定义好各个 policy ,然后可以通过模板实例化得到像不同的组合类了:
CPT<RandomSubtreePolicy, VwRegressorPolicy> cpt;
CPT<BalancedSubtreePolicy, VwRegressorPolicy> cpt2;
注意类的数量是没有减少的,只是现在我们使用了模板,让编译器来帮我们生成代码,从而避免了不必要的重复。当然,顺带地,使用这种类似
functor
的方式的效率会比使用虚函数要高一些。当然啦,由于使用了模板,模板的所有缺点也被引入进来了:代码必须放在头文件里(除非你很想折腾一下
export )、编译速度减慢、编译错误不知所云等等。另外,虽然
C++ 11 对于 functor 有了很多支持和改进,但是对于这种 general 的 policy
object 来说,支持却仍然比较原始,比如我没有办法在代码里要求我的 policy
object 里必须定义一个 which_subtree 函数,或者必须定义一个
regressor_t
类型,我只能假设它已经定义好了,直接去使用,如果它确实定义好了,那么就
OK
没问题,但是如果没有定义好或者和预期的不一样的话,就会出错,麻烦在于出错提示完全是随不同的编译器实现不同,并且有可能是风马牛不相及的。这种行为倒是很像动态语言里的
Duck Typing
,只是动态语言里一般不太会给几千页的 random error message 了。
其实原本有一个饱受期待的 C++0x 特性提议是用来解决这个问题的,那就是
C++
Concept,可惜的是在 09 年的时候这个东西居然被腰斩了,最终没有出现在
C++ 11 标准中。:(
如果是在动态语言中的话,就更加方便了,例如,在 Ruby 和 Python
中,我们可以通过 Mixin 的方式来实现 policy ,更美妙的是,在 Mixin
中还可以直接访问到被 mixin
的那个类的成员变量之类的。当然这只是一个语法包装,在 C++
里我们也可以显示地把 this 指针传到 policy object
的函数里去,并把 policy 类声明为 friend
来避免访问权限上的问题。不过即使只是一个语法包装也确实使得代码写起来方便了许多。
动态语言不需要 Concept 因为他们使用 Duck
Typing,方法可以动态地定义,并且还有 method_missing
这种可以拦截一切方法调用的大杀器。不过在静态语言里如果能够做这样的类型检查的话,会帮助避免很多潜在错误。虽然
C++ 里暂时没有了 Concept ,不过在 Google
Go 里倒是有类似的一个东西。在 Go 里有一个叫做 interface
的东西,类似这样:
- type SubtreePolicy interface {
- WhichSubtree() bool
- }
看起来和 Java 之类的 interface 好像很类似,一个接口里指定了一些未实现的方法。不过其实却很不一样,因为 Go 里的接口和 OO 的类型 hierarchical 没有任何关系,当然 Go 里面本身也没有这种 OO hierarchical 。这里的一个 trick 是:在 Go 里你并不需要显式地说明一个东西实现了某个接口,只要你的类型实现了接口里指定的那些方法,就算自动实现了该接口,所有接受该 interface 类型的参数的函数都可以传递一个你自己的对象进去。所以说它更像 Duck Typing 一些,然而却又有严格的编译期类型检查,这应该也正是 C++ 的 Concept 想要做的事情吧。
Go 本身确实也是很有意思的语言(除了它的 Logo
有点丑之外-.-bbbb),并且也已经发布了 1.0 版,从而使得
Language Specification
稳定下来了。等各种周边的库丰富起来之后,也许真的该好好看一下呢。我现在对
Go 的了解非常少,特别是不知道它的函数调用是怎么弄的,因为它的 closure
把函数的局部变量给 capture 出来似乎也是没事的。由于 Go 里很重要的一个
feature 是它的那个 Channels ,所以也许它的函数调用并不是像传统的 C
语言系的那样在 stack 上来弄的也不一定呢?
Comments
请教LZ,一般写一篇文章需要多久,用什么工具写的?貌似我写的长度是你的一半,就用了将近2个小时了...
工具?如果你是问编辑器的话随便什么都行,我一般用 vim ,如果你是问这个网站的话,我用的是 ruhoh.rb 。
写文章是比较费时的,取决于自己对文章内容的熟练程度,有些比较复杂的文章的话可能要断断续续地写几天吧,普通的文章几个小时也是肯定需要的。呵呵,写 blog 也是一个自己总结学习的过程么~如果几下子就写好了的话文章的意义可能就不是很大了吧? :p
嗯,我问的是前者,刚才看到你那篇介绍折腾博客的长文了~看来我还是缺乏写长文的耐心,再次膜拜下大牛,在MIT的生活还好吧~
还没开学呢,目前还在国内。
在ruhoh首页的使用者列表里看到你的头像,就顺着追过来了,果然是你 :-)
哈哈,你在尝试 static blog generator?ruhoh 还不错的,感觉比 jekyll 好用,特别是配合 rack-livereload。不过我现在这个 site 是为了定制方便已经完全是自己重新的一个 engine 了。其实整个就跟一个 make 系统差不多,依赖关系搞好就好办了~
我在GitHub上建了一个Jekyll+Bootstrap的blog。但是Markdown的局限实在太多了。不得已还得再折腾一回。你现在用的markup是你个人github上的这个吗? https://github.com/pluskid/...
不是,那个被我废弃了。我现在用的 pandoc 扩展过的 markdown,主要是为了支持公式显示。另外用 Liquid 定制一些 tag 作为复杂的结构,例如 figure 呀、边注呀、交叉引用、语法高亮之类的。render 的时候先用 Liquid render 出 tag,然后用 pandoc render 出 HTML 或 TeX,然后在用 Liquid 把 layout render 上就得到结果,这样组合起来基本上需要的任务都能完成。
不知道你碰到的 markdown 的局限主要是哪些?pandoc 还是挺好用的。我觉得也算 Haskell 的一个 killer app 了。
Pandoc确实很不错。Markdown的问题主要是自身的语法不可扩展,自身的语义又不够丰富,不能很好表达常见文档结构,而且不像reST那样可以在markup中自定义CSS class,从而导致样式控制非常死板。用Markdown的时候它给我的感觉就是:here's your dog food, eat and shut up.
可定制的话确实是没办法,Pandoc 虽然可以定制,但是对于 markup 本身好像不太好改,而且 Haskell 编程本身就比较恐怖了,所以组合着 Liquid 来用的话还是不错的。reST 我感觉写起来太麻烦的样子。如果需要写的时候支持定义 css class 的话,也可以试试 textile。不过我自己的话,把常用的 tag 归纳出来用 Liquid 实现之后用 markdown 也挺方便的,对于 blog 这种内容和格式都相对固定的话,还是不错啦。不过如果想要完全 flexible 的 HTML 页的话,就没办法了,不过那样的话差不多都可以直接写 HTML 了。哈哈……
代码放到GitHub上吧~求参考 :p
代码发给你邮箱了,本来 generator 的代码公开无所谓的,但是我目前 blog post 的代码是混在一起的,暂时不想公开这个,又懒得费力把这两者分开,于是目前放在 bitbucket 的 private repo 上的……
Thanks very much~ 不好意思在评论里扯了这么多跟博文完全无关的额内容……
那些C++ template的书确实比较BT,Modern C++ Design 看了都没用过,这里用policy-based解决方案倒是不错!最近刚好用到boost thread,然后接触了boost::bind,是个好东西。可以直接都类成员函数包装成函数接口,传递给需要函数接口的地方(如果是需要C函数指针的地方,要折腾些,不过总比静态(或全局)函数好。没仔细研究过,不知道boost::bind是怎么把私有成员函数包装成函数接口的。
PS: 前段时间刚好也看了下GO语言,感觉interface和channel相当有新意,前者可以很好解决函数接口的方法,于是strategy design mode就可以下岗了,很多依赖继承的方案也可以换思路了(少点继承是个好事,设计还是简单直接点的好)。后者用来写多线程一定不错,可惜可以用的库太少了,也没有界面库。没实际用过GO,胡扯下。
boost::bind 可以尝试用 C++ 11 提供的原生 lambda 替代。boost 的东西都是一出错就会很崩溃。曾经 boost::bind 和 boost::asio 搭在一起用的时候有些编译错误怎么都看不明白是哪里错了……
考虑兼容,现在还不能用c++11。向你求教一个多线程的问题:主线程A启动一个线程B来一个比较耗时的操作,当B 结束后,得到的数据需要在线程A中处理,如何在B结束时及时通知A呢?我想到的是在A中定时查询B是否结束了。WIN32 API可以用PostMessage传递消息,猜想也是有一个message queue在定时查看的。
线程 A wait 在一个 condition 之类的东西上就可以了啊,不过我猜你是希望线程 B 在计算的时候线程 A 还继续执行?这样行为看起来有点奇怪的样子。
A线程是GUI,所以不能wait,有什么好方法不?
GUI 的话,本身就是事件机制呀,监听按键呀之类的,所以给他发送消息就好了吧。
如果有WIN32 API这样没问题,只是GUI是自己画的,按键事件由类似函数指针的东西完成,所以也用不上呀。不知在多线程编程方面有相关技术实现这个不?记得Qt的QThread中有一个finished sinal,就不知道可不可以跨线程。
Qt 的话不是就更方便了,它提供了语言级别的 Signal/Slot 来处理事件呀。
是的,可惜用的不是qt呀
自己画的 GUI 的话,因该也有事件循环机制的呀
16年 Concept 又一次被腰斩 _(:3」∠)_
en.......