返回   cpper编程论坛 > C/CPP/TMP/GP
注册账号 论坛帮助 会员列表 日历事件 搜索 今日新帖 标记版面已读

回复
 
LinkBack 主题工具 显示模式
  #1 (permalink)  
旧 2005-07-20
普通会员
 
注册日期: 2004-04-30
帖子: 62
TiGEr.zZ 正向着好的方向发展
默认 C++模板元和函数式编程实践

C++模板元和函数式编程实践

[前言]
不知不觉,C++这门曾经受到很多人追捧的语言如今已经变得门庭冷落,相反倒是可以认为是其缩减版本的Java、C#却不断蓬勃发展壮大起来。不可否认,Java,乃至C#所基于的.Net环境有着传统语言无法比拟的优势,但是否真得如同很多人想象的一般投身于Java/C#就意味着生产力的极大提高,究竟是语言本身抑或是其他什么原因导致了这种变化?
对于靠编写代码为生,有过不同环境和语言开发经历的人而言,仔细回味其中的异同,恐怕有无尽的酸甜苦辣。由主观而言,我不太愿意去分辨其中的高下优劣,但客观上却也难失偏颇。故而秉着实践的原则,从语言本身所影响的侧面介绍现今环境下C++编程领域中一些“常用”的手法。
本来是希望完成上一篇《搭建自由桌面开发环境》后才开始这篇文章,一是有个循序渐进的过程,二是希望能够好好准备一下,但看来我的安排无法适应这个环境变化的频率,而且过于呆板的方式是有悖于人类的好奇心的,也就很大可能不是最佳的学习方式,故而就先写些东西再说吧,在过程中加以修正,所谓“敏捷”方法。同时内容可能会有先入为主,考虑不周的地方,甚至显得“无知愚蠢”。大家可以尽管讨论批评,不要在乎我的面子。
[又一门新语言]
“把标准C++当作一门新语言来学习”这是C++创始人Bjarne Stroustrup反复强调的一件事情,虽然说这句话相对非标准C++时代已经比较晚了,而且标准C++推广至今也有些年头了,但不得不承认,这句话的意义在今天仍然非同寻常。
和Ruby这样曾被称作“最少惊奇语言”(就是说不同人用它几乎都是以相同(类似)代码完成同一件事情)不同,C/C++的代码一向以令人惊讶著称,比如“一句话”的代码。这看来不是一个好特性,不然也没人会如此命名Ruby。但不得不承认,这种惊讶往往还是会引起程序员的注意的,毕竟有着在创作代码上的突破不正是我们所希望的,难道你真的希望写出来的代码如同白开水一般,如果那样的话,选择程序员这个职业这辈子可真是没指望了。只是往往这些令人惊讶的代码并非有着本质的区别,除了让人感到困惑之外,并没有带来什么长久的惊喜。
C++继续延续着这样的情景,直到Java的出现,和过往的新语言不同,初始的Java除了改善了环境,吸收了一些其他语言的特性之外,几乎就是C++的简化版。就是这个“青出于蓝而不胜于蓝”的家伙竟然吸引了如此多的眼球。看来C++也的确为此付出了代价,因为Sun本来并不打算冒风险去搞一门新语言。即便今天看来,Java的成功还是具有很大的偶然性的。
优点往往和缺点是共存的,在于我们如何扬长避短,去芜存菁。随着标准库的应用和推广,人们逐步认识到这种语言的又一个新的令人惊讶的特性,这就是STL。在2000年前后,很多人提起STL是如何得好用,直到现在。虽然这其实应该是很多年前就存在的东西,为什么这么受人欢迎的东西却应用的这么晚,这和现在Java世界不断有新的库的涌现,不断有人对新特性的鼓吹形成的鲜明的对比?个中原因不说想必大家也很清楚。
那么如今,我们这些还在用C++的“土人们”还有什么可以拿得出手的可以和主流开发语言相抗衡的特性吗,还是真的到了该撤退的时候了?好的,那就让我们来看看吧。
[C++异常句柄]
我们从比较简单也可能比较常见的问题说起,在标准C++中引入了结构化的异常处理方法。这虽然大大简化了程序在处理异常情况下的复杂性,但是还是存在着一些设计和实现上的困难。因为try...catch这样的语句毕竟不是全局的,而设计上往往需要一个全局一致的异常处理和管理方案,所以提出异常句柄来处理这样的问题。
利用C++特别的异常处理特性,不带参数的throw语句可以继续抛出捕获的任何异常,当然与此对应的就是捕获所有的异常,文(1)中提出了一种可重用异常句柄,这里简单示例一下:
代码:
/* 代码中Exception1,Exception2是异常的类型,在C++中可以是任意类型 */ void ExceptionHandler() { try { throw; } catch (Exception1& e) { // ... } catch (Exception2& e) { // ... } // ... }
[提醒]在VC提供的代码中多喜欢用throw new Exception()这样的方式抛出异常,对应捕获的异常类型就应该是Exception *,虽然这和上面一样都没有复制对象开销,但有一个释放问题,会导致内存泄漏。
在客户代码中:

代码:
try { // 需要捕获异常的处理代码 } catch(...) { ExceptionHandler(); }
这样就把所有的异常处理集中到一起了。进一步的来看,因为实际中,我们处理的异常和处理的方式肯定是在不断变化的,这导致句柄本身的变化。而且一个很大粒度的句柄还是存在不够灵活的地方。因此我们就句柄的实现作进一步的探讨。
如果把句柄中的try { } catch { } catch { }这样的语句看成switch( ) case { } case { }语句,那么这就是一个典型的Visitor模式应用的场合,通过Visitor模式,我们可以重新组织我们的句柄函数为一个对象,具有 多个更小粒度的处理单个异常的函数(方法)入口。这样我们只需要往这个对象中添加函数,修改函数,而不会对整个对象作大的改动。当然因为异常本身 不大可能都继承自定义好的Acceptable接口,我们需要一个中立的对象或方法来实现分派,这样还有一个好处就是代码更加紧凑。当然不可能还是上面这样的非循环方式(Acyclic Visitor),而且最好保持静态类型安全。所有这些需求,都已经有了现成的答案,这就是Andrei Alexandrescu在他的书中(2)详细介绍的Typelist的应用之一,我们直接借用Loki库(3)中的代码,并对其中的Visitor实现稍作修改:
代码:
#include <loki/Typelist.h> template <class TList> class ExceptionHandle; template <class T> class ExceptionVisitor { public: virtual void onException(T&) const = 0; virtual ~ExceptionVistor() { } }; template <class Head, class Tail> class ExceptionHandle<Loki::Typelist<Head, Tail> > : public ExceptionVisitor<Head> , public ExceptionHandle<Tail> { using ExceptionVisitor<Head>::onException; public: void operator()() const { try { throw; } catch(Head& e) { this->onException(e); } catch (...) { ExceptionHandle<Tail>::operator()(); } } }; template <class Head> class ExceptionHandle<Loki::Typelist<Head, Loki::NullType> > : public ExceptionVisitor<Head> { public: void operator()() const { try { throw; } catch(Head& e) { this->onException(e); } } };
关于其中Typelist使用的细节我就不介绍了,请参考相关书籍和文章。主要就是用了递归法,这也是目前的元编程中的常用方法之一,我们后面也会用到。上面的代码把分派和访问者集中在一个函数对象中,而且分派就是函数方法(重载()操作符),这样刚好符合我们句柄的特性,继续往下看。
这样的话,我们的客户方代码需要定义异常句柄就非常直接和简练了,比如我们要定义处理两个异常类型Exception1,Exception2的句柄:
class SomeExceptionHandle
: public ExceptionHandle<TYPELIST_2(Exception1, Exception2)>;
然后编译器会提示你实现纯虚函数onException(Exception1 &)和onException(Exception2 &):
代码:
virtual void SomeExceptionHandle::onException(Exception1& e) const { // ... } virtual void SomeExceptionHandle::onException(Exception2& e) const { // ... }
[提示]上面的代码中两个异常之间最好没有继承关系,因为在这种情况下编译器不提示类型转换,C++的explicit仅用于构造函数。如果你希望有进一步的安全性,可以尝试boost::type_traits及类似的解决方法。或者不花这个脑筋而改善其他地方,不过对于年轻学生而言,这是你的思考题。

到目前为止,我们的设计目的大部分已经达到,我们简化了异常定义(ExcptionHandle模板),抽取了异常类型(模板参数Typelist)以及一个重用的分派方法,同时有了一定的静态安全保证。
但是还有一个问题,虽然一个句柄的代码粒度被进一步细化,但句柄本身的粒度仍然没有被改变,我们需要进一步的工作来达到重用代码和适应变化的目的。在实际中我们可能定义了多个句柄函数或函数对象,我们需要对他们能够自由组合,而仅仅定义(需求上)最小粒度的句柄。
好的,这就到了函数大显身手的时候了。
我们按照函数式的视点来考察句柄,用抽象的语言定义就是void()类型,一个没有参数没有返回值的函数,上面的函数对象模板保证了这个定义。那么多个这样的函数如何组合呢?这大概有两个方法,一个是把各个句柄对象看作值类型,在需要的时候组合成值串同样利用递归或者循环的方式有次序回调其中句柄,还有一个就更加直接一些的那就是函数组合了。两种方法实质一致,后一种因为使用boost::bind更加普遍和重要,所以我们采取它。
我们把各个句柄函数作为参数传递给一个包裹函数,然后由包裹函数作进一步的分派。参照原始的句柄函数,很容易实现,比如一个到两个句柄的包裹函数是:
代码:
#include <boost/function.hpp> void ExceptionHandleWrapper(boost::function<void()> exception_handle1) { exception_handle1(); } void ExceptionHandleWrapper(boost::function<void()> exception_handle1, boost::function<void()> exception_handle2) { try { exception_handle1(); } catch (...) { exception_handle2(); } }
这里使用boost::function来对寻常函数和函数对象作统一的封装。因为实际上涉及到不定个数的句柄,也就是若干个包裹函数,你可以一口气定义上很多个。我们这里参照boost里面标准方案预处理宏来处理,代码基本上都是直接粘贴的,但的确可以工作。
在主要的头文件中:
代码:
#include <boost/preprocessor/iterate.hpp> #include <boost/detail/workaround.hpp> #ifndef BOOST_FUNCTION_MAX_ARGS # define BOOST_FUNCTION_MAX_ARGS 10 // 最大参数个数 #endif // BOOST_FUNCTION_MAX_ARGS # if BOOST_FUNCTION_MAX_ARGS >= 0 # define FUNCTION_NUM_ARGS 0 # ifndef EXCEPTION_HANDLE_FUNCTION_0 # define EXCEPTION_HANDLE_FUNCTION_0 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 1 # define FUNCTION_NUM_ARGS 1 # ifndef EXCEPTION_HANDLE_FUNCTION_1 # define EXCEPTION_HANDLE_FUNCTION_1 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 2 # define FUNCTION_NUM_ARGS 2 # ifndef EXCEPTION_HANDLE_FUNCTION_2 # define EXCEPTION_HANDLE_FUNCTION_2 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 3 # define FUNCTION_NUM_ARGS 3 # ifndef EXCEPTION_HANDLE_FUNCTION_3 # define EXCEPTION_HANDLE_FUNCTION_3 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 4 # define FUNCTION_NUM_ARGS 4 # ifndef EXCEPTION_HANDLE_FUNCTION_4 # define EXCEPTION_HANDLE_FUNCTION_4 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 5 # define FUNCTION_NUM_ARGS 5 # ifndef EXCEPTION_HANDLE_FUNCTION_5 # define EXCEPTION_HANDLE_FUNCTION_5 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 6 # define FUNCTION_NUM_ARGS 6 # ifndef EXCEPTION_HANDLE_FUNCTION_6 # define EXCEPTION_HANDLE_FUNCTION_6 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 7 # define FUNCTION_NUM_ARGS 7 # ifndef EXCEPTION_HANDLE_FUNCTION_7 # define EXCEPTION_HANDLE_FUNCTION_7 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 8 # define FUNCTION_NUM_ARGS 8 # ifndef EXCEPTION_HANDLE_FUNCTION_8 # define EXCEPTION_HANDLE_FUNCTION_8 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 9 # define FUNCTION_NUM_ARGS 9 # ifndef EXCEPTION_HANDLE_FUNCTION_9 # define EXCEPTION_HANDLE_FUNCTION_9 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif # if BOOST_FUNCTION_MAX_ARGS >= 10 # define FUNCTION_NUM_ARGS 10 # ifndef EXCEPTION_HANDLE_FUNCTION_10 # define EXCEPTION_HANDLE_FUNCTION_10 # include "detail/exception_handle_template.h" # endif # undef FUNCTION_NUM_ARGS # endif
[旁白] 唔,看起来的确是化繁就简,哦,不是,是去简就繁了!ooO...其实呢,是杀鸡用了宰牛刀。对比boost的正常处理方式,这里的宏也没有用彻底,就是后面这一长串,考虑到宏的不可读性,我还是喜欢拷贝粘贴。
然后在detail/exception_handle_template.h中定义:
代码:
#include "prologue.h" #include <boost/function.hpp> #define FUNCTION_PARM(J,I,D) boost::function0<void> BOOST_PP_CAT(a,I) #define FUNCTION_PARMS BOOST_PP_ENUM(FUNCTION_NUM_ARGS,FUNCTION_PARM,BOOST_PP_EMPTY) #define FUNCTION_ARGS BOOST_PP_ENUM_PARAMS(FUNCTION_NUM_ARGS, a) namespace { #if FUNCTION_NUM_ARGS == 0 void ExceptionHandleWrapper(boost::function<void()> exception_handle) { exception_handle(); } #else // Comma if nonzero number of arguments void ExceptionHandleWrapper(boost::function<void()> exception_handle, FUNCTION_PARMS) { try { exception_handle(); } catch (...) { ExceptionHandleWrapper(FUNCTION_ARGS); } } #endif } #undef FUNCTION_ARGS #undef FUNCTION_PARMS #undef FUNCTION_PARM
同样我就不介绍宏实现的细节了,可以看得出这里是一个宏的“递推”的应用。
[注意]请注意其中的namespace关键字,这里定义了一个匿名的命名空间,在C++中推荐使用匿名命名空间代替static关键字。因为这里定义的是寻常函数,而寻常函数缺省情况下是extern暴露的,这样在多个源代码包含这个头文件的时候,会产生多个目标文件中包含重复函数定义而导致链接失败。(为什么知道的这么精确?ooO...呵呵,就是我犯过错啦!)
好的,那么这个ExceptionHandleWrapper又该如何被客户端代码使用呢?终于到了boost::bind出场的时候了,我反复强调boost::bind,并不是它有多么令人惊讶,实际上它很常见。但正因为常见,所以重要,而我强调的意义在于乐于看到大家使用它,后面大家就可以看到它贯穿始终。
我们知道不论怎么样的异常句柄都应该被客户端来调用,而句柄的函数类型也应该就是void(),但这个Wrapper则不满足这样一个类型,因为它需要真正的句柄为其参数。客户端代码往往不知道该如何去生成参数,它需要的只是句柄本身。在Java中为了达到类似的目的,我们不得不借助于对象化,然后初始化,IoC框架等重磅手段。但boost::bind帮我们轻松的化解了这样一个难题。
比如除了上面我们定义了一个包含两个特定异常处理方法的句柄外,我们还定义了两个更加一般化的异常句柄,一个捕获标准C++异常,这样通过其what接口可以看到异常的内容(大家在定义自己异常的时候,都应该继承这个接口,虽然C++社区没有大肆宣传接口,但不得不承认,有的时候,这个世界还是需要一致性的);另外一个捕获未知异常,这样一些可能是其他方面发出的异常也可以被了解到,作为我们异常处理的最后一道屏障,虽然它在运行时往往帮不上大忙。
代码:
// ... #include <iostream> #include <stdexcept> class StdExceptionHandle : public ExceptionHandle<TYPELIST_1(std::exception)> { public: virtual void onException(std::exception& e) const { std::cout << "Standard exception catched: " << e.what() << std::endl; } }; void UnknownExceptionHandle() { std::cout << "Unknown exception catched!" << std::endl; }
然后我们的客户方代码可以把三个异常句柄组合起来使用,在可实例化句柄对象和句柄参数(如果有的话,这里没有)的地方:
代码:
#include <boost/bind.hpp> // some code here... try { // some code here... } catch (...) { ExceptionHandleWarpper(SomeExceptionHandle(), StdExceptionHandle(), &UnknownExceptionHandle); } // some code here... doSomeFunction(boost::bind(&ExceptionHandleWrapper, SomeExceptionHandle(), StdExceptionHandle(), &UnknownExceptionHandle)); // some code here...
其中在其他地方的doSomeFunction它无法实例化上面的句柄,或不可见其定义,但接受一个void()的函数类型异常句柄作为其参数(之一):
代码:
void doSomeFunction(function<void()> exception_handle) { try { // some code here... } catch(...) { exception_handle(); } }
呵呵,这对初次听说的人而言是不是觉得很可笑:为什么可以接受这么一个函数,而不能接受上面几个实例或定义?
当然在函数式编程中还有更加“自然”的需求,我们这里只是举比较简单例子。但就事论事,原因之一就是“变化”:你不能确定究竟最终会传递多少个参数过去,或者究竟需要多少个类型,多少个实例,更不用说传递这些参数的繁琐了。那么一个函数就没有变化了?嗯,就这个需求看,的确如此!因为变化都被提升了,或者说滞后处理了,单个函数因为其接口的粒度小,简洁,很容易稳定下来,这里的void()就是实例之一。如果采用策略模式(这么一说你应该理解个大概了吧),在doSomeFunction中通过信号的方式把异常抛出来处理,也是常用的一种处理方式。信号我们在后面会用到,这里就不展开讲,不过信号同样是基于函数的,也就是在语法上和上面一致。
提升变化,这也是Java世界大张旗鼓宣扬的IoC精神实质所在。直白的讲就是一切都是参数,都是具有可变性的。但按照传统的结构化编程模式,随着设计的不断细分,用参数传递的办法,很快代码就会膨胀到抵消细分带来的好处直至无法接受(当然在部分地方还是可以用的,Pico不是曾经宣扬通过构造函数来实现IoC,呵呵),有了boost::bind,这些都是小Case了。
[旁白]唔,上面的废话多了些,大家得看且看吧,看不明白的去后面抛砖头...
对于希望少敲一些键盘同时喜欢整洁的人的而言,上面的函数看起来有些长,你很容易想象出有不同个数的bind调用,这些可以通过宏简化一点点:
代码:
#define EXCEPTION_HANDLE_1(a1) boost::bind(&ExceptionHandleWrapper, a1) #define EXCEPTION_HANDLE_2(a1, a2) boost::bind(&ExceptionHandleWrapper, a1, a2) #define EXCEPTION_HANDLE_3(a1, a2, a3) boost::bind(&ExceptionHandleWrapper, a1, a2, a3) #define EXCEPTION_HANDLE_4(a1, a2, a3, a4) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4) #define EXCEPTION_HANDLE_5(a1, a2, a3, a4 , a5) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5) #define EXCEPTION_HANDLE_6(a1, a2, a3, a4 , a5, a6) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5, a6) #define EXCEPTION_HANDLE_7(a1, a2, a3, a4 , a5, a6, a7) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5, a6, a7) #define EXCEPTION_HANDLE_8(a1, a2, a3, a4 , a5, a6, a7, a8) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5, a6, a7, a8) #define EXCEPTION_HANDLE_9(a1, a2, a3, a4 , a5, a6, a7, a8, a9) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5, a6, a7, a8, a9) #define EXCEPTION_HANDLE_10(a1, a2, a3, a4 , a5, a6, a7, a8, a9, a10) boost::bind(&ExceptionHandleWrapper, a1, a2, a3, a4 , a5, a6, a7, a8, a9, a10)
这里实在没有用牛刀的必要了,然后客户方代码就是:
代码:
// some code here... try { // some code here... } catch (...) { EXCEPTION_HANDLE_3(SomeExceptionHandle(), StdExceptionHandle(), &UnknownExceptionHandle); } // some code here... doSomeFunction(EXCEPTION_HANDLE_3(SomeExceptionHandle(), StdExceptionHandle(), &UnknownExceptionHandle)); // some code here...
嗯,偶尔用一下宏还是不错的,但注意定义的时候把他们区分开来,免得找不着北。
到这里我们实践的第一章就差不多结束了,但是还有一些疑虑,让我们考察其他方面的问题:
聪明的人在后半部分就会感到前后有重叠的地方 --- 既然我们只需要最小粒度的句柄,那么定义一个一个句柄函数,每个处理一个(种)异常,然后再包裹组合他们就可以了?
答案是的确如此,但是实际中我们也不是总喜欢小粒度的东东,至少还可以节省一些“粘合剂”。
[另话]异常处理是程序设计中的大问题,本文只是从一个并不十分重要的侧面去揭示异常处理的面纱,在实际庞大项目中,处理异常的重点往往并不在此。本文在很大程度上都有大材小用之嫌,但这反而有助于我们了解实践背后语言自身的特性。我希望是如此,也欢迎大家批评补充。
[思考题]在这篇帖子(4)中,语言天才ajoo为我们展示了异常处理的另一个侧面,引起了广大的反响,其中Elminster大人也有回应。请仔细辨析其重点所在,思考各种处理方式的优点和缺点,提出你所倾向的处理方式和理由。

参考链接:
(1)http://www.kotiposti.net/epulkkin/in...n-handler.html
(2)《Modern C++ Design》
(3)http://sourceforge.net/projects/loki-lib
(4)你擦了吗?确定擦了?真的确定擦了?
(5)Boost库
Digg this Post!Add Post to del.icio.usBookmark Post in TechnoratiFurl this Post!
回复时引用此帖
回复

书签

主题工具
显示模式

发帖规则
不可以发表新主题
不可以发表回复
不可以上传附件
不可以编辑自己的帖子

启用 BB 代码
论坛启用 表情符号
论坛启用 [IMG] 代码
论坛禁用 HTML 代码
Trackbacks are 启用
Pingbacks are 启用
Refbacks are 启用



所有时间均为格林尼治时间 +9。现在的时间是 09:45 AM


Powered by vBulletin® 版本 3.7.0
版权所有 ©2000 - 2009,Jelsoft Enterprises Ltd.
(C) Copy Right All Right Reserved 2001 - 2007

Search Engine Friendly URLs by vBSEO 3.1.0