huaxiazhihuo

 

2017年1月14日

预处理的图灵完备之引言

好久没有光顾cppblog了,现在这里这么冷清了,不免让人有些伤感,可见c++现在多么的不得人心,也可能是c++的大神去了其他的网络平台,好比知乎。不管怎么样,始终对c++还是有些感情,也对cppblog有些感情。

我们还是来讨论c++吧,这几年在c++里面玩代码自动生成技术,而预处理是不可避免,也是不可或缺的重要工具。虽然boost pp预处理库在宏的运用上很是完善,但是代码也太多了,而且代码很不好理解,对此,不免让人疑惑,有必要搞得那么复杂,搞那么多代码吗?并且,看了boostpp的使用接口后,感觉写得很不干净,也不好组合。因此,重新做了一套预处理的轮子。以下的代码,假设在msvc2013以上的版本运行,反正很多人用MSVC的,装逼的自当别论,造出来的轮子,倾向于先支持msvc。

首先,我们定义一个宏,用来给把入参变成字符串,咦,这个事情也太easy了,但是,在此,感觉,还是有必要废话多解释一下。以下代码惯例都是,所有可用的宏函数都是以PP开头全部大写,而以_ZPP开头的全部都是内部实现,其实还可以做得更难看一点。因为宏函数是全局的,没有作用域的概念,并且只是单纯的文本替换,死的时候,还不知道怎么死,所以,必须谨慎对待。像是windows.h头文件那样,直接用min,max作为宏的名字,虽然用起来很方便,但也不知道制造了多少麻烦,所以,很多时候,包含windows.h时,第一件事情就是undef min和max。

以下的代码,可以随便在某个工程下,随便建立一个cpp后缀名的源文件,然后按CTRL+F7编译,不需要F5,就可以看到运行的效果,如果编译通过,就说明宏基本上正确,测试代码越多,准确性就越高。当然,你们也可以通过设置源文件的属性,让msvc生成预处理后的文件,然后用记事本打开那个文件观看。
#define PP_TEXT(str) _ZPP_TEXT(str)
#define _ZPP_TEXT(str) #str
在c++预处理宏中,操作符#是将后面跟随的表达式加上两个双引号,也就是字符串。PP_TEXT(str)不是直接定义成#str,而是通过调用_ZPP_TEXT(str),然后在那里才将入参变成字符串,显得有点辗转,有点多此一举,但,其实是为了支持宏的全方位展开,也就是入参str本身也存在宏调用的时候,纯属无奈。比如,如果这样实现
#define PP_TEXT(str) #str
那么,对于下面的情况,
#define AAA aaa
PP_TEXT(AAA),结果将是"AAA",而不是"aaa"。因为宏操作符直接是将入参变成字符串,没有让入参有一点点回旋的空间,所以只好引入间接层,让入参有机会宏展开。后面,很多宏函数都是这样实现,不得不间接调用,以便让宏全面展开。而msvc的宏展开机制更加奇葩,更加不人性化,其间接调用的形式也更丑陋。这都是没办法的事情。
然后,为了调试宏,或者测试宏,当然,很多时候,调试宏,还是要打开预处理的文件来对比分析。我们对 static_assert作一点点包装,因为static_assert需要两个参数,c++11后面的c++版本中,static_assert好像只需要一个入参,那时就不需要这个包装了。
#define PP_ASSERT() static_assert((__VA_ARGS__), PP_TEXT(__VA_ARGS__));
PP_ASSERT(...)里面的三个点,是不定参数的宏,而__VA_ARGS__就代表了...所匹配的所有参数,这条语法很重要,要熟练。这里,就不详细解释其用法了,后面会有大把大把的宏函数用到__VA_ARGS__。
好了,我们可以开始用PP_ASSERT(...)做测试了。
PP_ASSERT(2+3==5)
如果,然后编译这个文件,发现编译通过了,比如
PP_ASSERT(2+3==4)
编译的时候,就会报错误信息,error C2338: 2+3==4
好了,测试准备建立起来,就可以开始肆无忌惮的写代码了。一步一步地构建c预处理宏的图灵完备。
显然,当务之急,最根本的宏就是将两个宏参数的并接,也即是##运算符,显然好比#运算那样子,必须给里面参数有宏展开的机会,因此要间接调用,下面是其实现
#define PP_JOIN(_A, _B) _ZPP_JOIN_I(_A, _B)
#define _ZPP_JOIN_I(_A, _B) _ZPP_JOIN_II(~, _A##_B)
#define _ZPP_JOIN_II(p, res) res
竟然不止一层间接,而是两层,又多此一举,是因为发现在做宏递归的时候,一层间接调用还不能让宏充分地展开,所以只好又加间接层,也不明白是何原因,也懒得追究了。现在,接下来,当然是测试PP_JOIN了。各位同学,可以新建立一个测试文件,那个文件include我们的这个宏函数。当然,也可以在同一个文件里面写测试代码,注意分成两段代码,上一段写宏函数,下一段写测试代码,目前来看,都可以的,后面再整理。
PP_ASSERT(PP_JOIN(1+2== 3))
#define A 20
#define B 10
PP_ASSERT(PP_JOIN(A 
+ B, == 30))
有了PP_JOIN,就可以开始做点其他事情了。比如,计数器,
#define _ZPP_INC_JOIN(_A, _B) _ZPP_INC_JOIN_IMP1(_A, _B)
#define _ZPP_INC_JOIN_IMP1(_A, _B) _ZPP_INC_JOIN_IMP2(~, _A##_B)
#define _ZPP_INC_JOIN_IMP2(p, res) res

#define PP_INC(x, ) _ZPP_INC_JOIN(_ZPP_INC_, x)
#define _ZPP_INC_0         1
#define _ZPP_INC_1         2
#define _ZPP_INC_2         3
#define _ZPP_INC_3         4
#define _ZPP_INC_4         5
#define _ZPP_INC_5         6
#define _ZPP_INC_6         7
#define _ZPP_INC_7         8
#define _ZPP_INC_8         9
#define _ZPP_INC_9         10
这里,我们重新又实现了一遍PP_JOIN,这也是没办法的事情,后面在重重嵌套的时候,会出现PP_JOIN里面又包含PP_JOIN的情况,这样会导致宏停止展开了,所以,只好对于每一个要用到JOIN之处,都用自己版本的JOIN。
这是宏函数的实现方式,通过并接,文本替换,一一枚举,才达到这样的效果,也就是说,我们通过JOIN函数,在宏里面构造了一个计数器的数据类型。如果每个宏函数都这样写,岂不是很累。好消息是,只需用这种苦逼方式实现几个最基本的函数,然后通过宏的递归引擎,其他的宏函数就不需这样子一个一个苦逼的并接替换了。
PP_ASSERT(PP_INC(9)==10)
PP_ASSERT(PP_INC(PP_INC(
9)) == 11)
写测试代码习惯了,写起来就很有意思了,测试通过,也是最激动人心的时刻。
接下来,要处理msvc里面宏的恶心行为,然后就结束本引言。
#define PAIR_SECOND(x, y) y
PP_ASSERT(PAIR_SECOND(
1020== 20)
这样子,还不错,下面,再define一个宏函数,让其返回一个pair,也就是两个值
#define MAKE_PAIR(x, y) x, y
然后,这样调用,
PAIR_SECOND(MAKE_PAIR(1020))
编译器马上就不高兴了,warning C4003: “PAIR_SECOND”宏的实参不足
好像是编译器没有先展开MAKE_PAIR(10, 20),然后再调用PAIR_SECOND,而是直接把MAKE_PAIR(10, 20)整个当成一个函数传给PAIR_SECOND,然后,PAIR_SECOND就提示实参不足,然后,硬要测试,
PP_ASSERT(PAIR_SECOND(MAKE_PAIR(1020)) == 20)
显然,无论如何,编译器势必就龙颜大怒了。对此,我们只好再引入间接层,想办法让MAKE_PAIR(10, 20)先展开,然后再传给PAIR_SECOND。这样,就不能直接用这样的形式了,PAIR_SECOND(MAKE_PAIR(10, 20)) 。只好改成这样,下面的几行代码,很有点惊天地泣鬼神的味道。
#define _ZPP_INVOKE_JOIN(_A, _B) _ZPP_IMP_INVOKE_JOIN_I(_A, _B)
#define _ZPP_IMP_INVOKE_JOIN_I(_A, _B) _ZPP_IMP_INVOKE_JOIN_II(~, _A##_B)
#define _ZPP_IMP_INVOKE_JOIN_II(p, res) res

#define PP_INVOKE(m, args, ) _ZPP_INVOKE_JOIN(m, args)
前面几行代码都是PP_INVOKE的JOIN函数实现,可以直接当它们是JOIN函数,关键是PP_INVOKE(m, args, ...)这里,第一个参数m是宏函数,第二个是args,是要传给第一个参数m的参数列表,用括号括起来,至于后面的省略号,是有些时候为了取悦编译器而添加的,也不知道是什么原因,反正这样子就可以了,懒得追究。垃圾宏,垃圾预处理,只要能完成功能就行了,c++中,代码生成代码,重头戏在tmp那里,宏只是小小必要的辅助工具而已。然后,这样调用,
PP_ASSERT(PP_INVOKE(PAIR_SECOND, (MAKE_PAIR(10, 20))) == 20)
编译通过了,好不容易啊!

posted @ 2017-01-14 15:01 华夏之火 阅读(704) | 评论 (0)编辑 收藏

2016年5月20日

lisp的括号

       lisp(当然也包括scheme)的元编程(也即是宏)威力非常强悍,相比之下,c++的元编程(template+预处理)简直就是弱爆了,被人家甩几条街都不止。 当然,template的类型推导很厉害,也能生成很多签名类似的class和function,比其他语言的泛型强多了,但是,template再厉害,也不能生成名字相似的function还有变量。 预处理可以生成名字相似的变量和函数。但是,预处理的图灵完备是没有类型这个概念,只有字符串,整数那个东西还要靠字符串的并接来实现。所以,预处理没法得到template里面的类型信息。新版本的c++中有了decltype之后,宏可以通过某种方式以统一的形式来利用类型信息。但是,在代码生成方面,预处理还是很弱智,主要的问题在于宏对于自己要生成的代码结构很难构建语法树,也不能利用编译阶段的功能,比如调用编译阶段的函数。 想说的是,很难以在代码中只用宏来写一个稍微复杂的程序,即使做得到,也要吐好几口老血,还煞难调试。
       lisp就不一样了,宏和语言融为一体,以至于代码即是数据。只要你愿意,完全可以在lisp中只用宏写代码,只要愿意,分钟钟可以用lisp写一个dsl,比如loop就是一个专门处理循环的dsl。甚至,用lisp宏还可以做静态类型推导的事情,也非难事。因此,用lisp宏搞基于对象 (adt)也都有可能,从而优雅的使用.操作符。比如(+ obj1.item obj2.item) (obj.fun 2 "hello")。你说,lisp宏连.操作符都可以做到,就问你怕不怕。
       但是,宏再厉害,也不能随意地搞底层操作内存。恰好与c++相反,c++搞底层随意操作内存太容易了,但是元编程的能力就远远不如lisp了。
       emacs是最好玩的ide,注意不是最强大,猿猴随时可以写代码增加改变emacs的功能,马上见效,不需要任何配置,不需要重启。因此,elisp也是最好玩的语言了,因为最好玩的ide的脚本语言就是它了,呵呵,主要原因还是elisp是lisp的方言,可以承担lisp的很多构思,当然,完全继承是不行的,不过,已经足够做很多很多的事情了。
       不过,本期的话题是lisp的括号,为什么lisp会有那么多的括号,铺天盖地,很容易,就一堆一堆的括号扰人耳目,以至于lisp代码不好手写,只能忽视括号,依靠缩进。括号表示嵌套,相必之下,c系语言的嵌套就没那么恐怖了。一个程序,顶破天,最深层都不会超过十层,连同名字空间,类声明,函数,再到内部的for,if,大括号,中括号,小括号。
       中缀表达式,这个众所周知了,试比较,1+2-3*4/obj.width,没有任何括号,依靠运算符优先级表示层次关系。并且,猿猴也习惯并本能的解析中缀表达式了,因此,代码看起来一目了然。lisp就很可怜了, (- (+ 1 2) (/ (* 3 4) (obj-width obj))),这里面多了多少括号,在转换成这行简单算式的时候,还是在emacs下面写出来的。关键是,虽然前缀表达式没有任何运算级别上的歧义,但是,人眼还是比较习惯中缀表达式了。君不见haskell的括号更少了,其对中缀表达式和符号的运用更深入。关键是,中缀表达式很容易手写啊。易写,自然也表示易读。C#的linq的深受欢迎也因为其好读,无须在大脑里面建立什么堆栈,linq表达式就是上一个处理的结果通过.操作符传递到下一个运算中,非常顺畅,不必返回前面去看看当前的操作数的运算是什么,因为运算符就在眼前了。中缀表达式.操作符,更是灭掉括号的大杀器,比如,obj.child1.child2.value,这里用lisp来搞,4个括号避免不了的。
       试试将java万物皆是对象推向极致,然后没有中缀表达式,1.plus(2).minus(3.mult(4).obj.width)),比lisp要好一些,但也有很多括号了,并且,在minus这里,其括号嵌套也只是减少了一层而已。当表达式复杂起来的时候,这种缺点也要相应的放大。
       变量的就地定义,好像c系的变量要用到的时候才定义这种语法很稀疏平常,没什么了不起的。但是,到了lisp下面时,就知道这是多么贴近人心的便利啊。每次用到新变量,都要引入let表达式,又或许跑到前面的let语句中写变量,要么就打断当前的代码编写,要么就引入新的一层嵌套关系。一个状态复杂的函数,很容易就出现好多个let语法块。而c系的变量就地定义,显得那么淡定。
       return,continue,break等语句就可以把后面的语句拉起来一个层次,假设没有这些关键字,要用if else语句,那么,这些return,continue,break后面的语句都表示要被包含在一个else的大括号中。
       lisp里面的特有语句,with-*等宏,都要求嵌套。几个with-*宏串起来,几个括号嵌套关系就跑不了啦。而c++通过析构函数就多么地让人爱不释手了,java也可以别扭的用finally来应付了。
       控制结构的并行。像是if,for,while或者是class还有函数定义等语句,其后面的代码块是并列在关键字的后面,这样就少了一层嵌套。不过这个作用并没有那么巨大。主要还是前面4点。
       这样,就可以模拟其他语言的特性来灭掉lisp的括号。当然是要到宏了,loop就是一种尝试。但是,下面将走得更远。其实,就是设计一套新的语法了。
       假设这个宏的名字是$block,那么后面的文章就可以这样做。
       1 加入一个$操作符。$表示后面的代码都被收入进去。比如,1+2-3*4/4,就可以写成($block (-) $ (+ 1 2) (/) $ (* 3 4) 4)。于是,with-*等宏的嵌套就可以用$来代替了。虽然,$的作用好像有些欠缺,功能不完备,但是,只要考虑到括号都是在最外层体现的,那么,$就显得很有作用了。
       2 加入let的操作符,表示就地引入变量,其实也即是将变量名字加入上层的(let)的变量列表中,然后在这里插入一条(setq var vlaue)的语句。
       3 加入if,elif,else,for,switch等语句,于是后面的代码块就与之平行了,并且准备一个(let)的语句,用于给with语句添加变量。可以借鉴loop宏的方式
       4 return,break,continue等相应的实现。
       5 支持.操作符,所有关于.的操作,都转换成相应的函数操作,好像以前的cfront在对于成员函数的支持那样子。这里就要有静态类型推导了,可以通过with语句中加入变量的类型说明,给函数添加返回类型的标签。有了这些信息,就可以找到obj相应的成员函数的名字,就可以生成对应的函数调用的form了,这个做起来有点难度。
       ......
       以上,除了第5点,其他都可以借鉴loop的代码来实现。$block里面的代码,便于手写,括号也没有那么面目可憎了。

posted @ 2016-05-20 11:17 华夏之火 阅读(875) | 评论 (0)编辑 收藏

2016年5月14日

迭代器的抽象

      迭代器是好东西,也是猿猴工具箱里面的七种武器之一。代码中必然要操作一堆数据,因此就要有容器,有了容器,自然就不可缺少迭代器,没有迭代器,容器使用上就会非常不方便,并且还必须暴露其内部实现方式。比如,在可怜的C语言里面,操作数组容器就要通过整数索引来访问其元素,操作链表容器,就要通过while(node->next!=null)这样的方式来访问其元素。某种意义上讲,整数索引,node->next这些都是迭代器,只是它们的使用方式没有统一起来而已。既然如此,全世界的迭代器的使用方式都统一起来,那么这就是迭代器了。
      基本上,现代化的语言,都会在语言层面上提供foreach之类的语法糖,其形式不外乎是,foreach(e:c){}。就是这样,只要提供元素的名字和容器对象。后面跟着循环体。其思想就是从容器里面取出一个元素,用循环体对这个元素进行操作。循环完毕,就完成了对容器里面数据的操作。这种语法形式,简洁得不能再简洁了。很好很方便,什么额外重复的代码都不劳费心了,甚至连类型推导不用做。真的,类型可以推导的时候,就让编译器推导好了。代码里面必须大规模的使用auto,var这样的关键字。不要担心看不出来变量的类型。变量类型应该从变量的名字中体现出来其抽象意义,当然,不要搞什么匈牙利命名法,那个太丑陋了。
      既然语法糖提供了这种对迭代器的支持操作语法,自然而然,只要涉及到一堆数据这样的概念,不必局限于具体的容器(数组,链表,哈希表),文件夹也是一堆数据,Composition模式也是一堆数据,数列,……,等等所有这些,全部都是概念上的一堆数据,只要提供了迭代器,猿猴就可以很优雅的用foreach这样的语法糖来统一操作数据,多么方便,多么的多态。不管这一堆数据的内部实现方式是什么,后期怎么修改,在foreach这里代码全部都不会受影响。更何况,对于迭代器,语法上不仅仅提供foreach的便利得无以复加的甜糖,还有一大堆的标准库函数来让猿猴操作迭代器,什么排序,查找,映射……。更令人发指的是,C#把迭代器捣鼓得好用得让人伤心难过悲愤欲绝,而linq语法上还可以把IEnumbale整成monad,可以用来作什么cps的变换。迭代器在手,天下我有。
      迭代器这个概念的抽象似乎很理所当然,但是不然,比如,刘未鹏举过Extended STL的例子,操作文件夹。C和C++代码对比。
// in C
DIR*  dir = opendir(".");
if(NULL != dir)
{
  
struct dirent*  de;
  
for(; NULL != (de = readdir(dir)); )
  {
    
struct stat st;
    
if0 == stat(de->d_name, &st) &&
        S_IFREG 
== (st.st_mode & S_IFMT))
    {
      remove(de
->d_name);
    }
  }
  closedir(dir);
}
 
// in C++
readdir_sequence entries(".", readdir_sequence::files); 
std::for_each(entries.begin(), entries.end(), ::remove);
显然,前者是没有迭代器的抽象,后者是有迭代器抽象的简洁异常的代码。第一次看到,惊为天人,其实本就该如此,只是C将这一切搞复杂了。当然,还有一批C 粉反对,说什么代码不透明了,隐藏了代码背后可能的复杂实现。对于这一簇人的坚持不懈反对抽象的态度,真不知该说什么好呢?代码的能力里面,最最重要的事情就是抽 象,通过抽象,猿猴才可以避开细节,将精力集中于更加重要更加复杂的事情。通过抽象,可以减少重复的代码,可以提高类型安全。C++是唯一能在玩抽象概念的同时,又可以兼顾到底层细节的处理,从而不仅能写出高效代码,还能玩出更炫的技巧。很多时候,必须底层玩得越深,抽象的触角才能伸得越高。
      其实,迭代器不必依存于容器。而是,先有了迭代器,才会有容器。请谨记,迭代器可以独立存在。begin和end就代表了一堆数据的概念。至于这一堆数据是如何存放的,这一切都无关紧要。基于此,有必要用class来表达一堆数据这么一个通用性极高的概念。其实,boost里面好像也有这么一个东西。就叫做DataRange吧。为何不叫Range,因为Range另有更重要用途,这么好的名字就是用来生成DataRange,代码不会直接看到DataRange,都是通过Range来生成DataRange。
template<typename Iterator>
struct DataRange
{
    typedef Iterator IteratorType;

    DataRange(IteratorType beginIter, IteratorType endIter) : mBegin(beginIter), mEnd(endIter)
    {
    }

    IteratorType begin()
const { return mBegin; }
    IteratorType end()
const { return mEnd; }
    IteratorType mBegin;
    IteratorType mEnd;
};
      然后,随便搞两行代码试试
   vector<int> vvv = { 1, 2, 3 };
    for (auto i : Range(vvv))
    {
        cout << i << endl;
    }
      其实,C++11概念上就支持一堆数据的操作,只要一个类型struct或者class里面有begin()和end()这一对活宝,并且这一对活宝的返回类型是迭代器,那么就可以尽情的享用foreach的甜糖。那么,何谓迭代器。就是支持三种操作的数据类型:!=(判断相等,用来结束迭代操作),前++(用来到迭代到下一个元素),*(取值)。那么,这就是迭代器了,显然,指针就是原生的迭代器。虽然,整形int也可以++,!=,但是不支持取值操作,所以int不是迭代器。下面就要把int变成迭代器。
template<typename Ty, typename Step>
struct ValueIncrementIterator
{
    typedef ValueIncrementIterator ThisType;
    typedef Ty ValueType;
    typedef Step StepType;

    ThisType(ValueType val, StepType step)
        :mValue(val), mStep(step){}

    
bool operator != (const ThisType& other) const
    {
        
return mValue < other.mValue;
    }

    ValueType 
operator* () const
    {
        
return mValue;
    }

    ThisType
& operator++ ()
    {
        mValue 
+= mStep;
        
return *this;
    }

    ValueType mValue;
    StepType mStep;
};
然后,再用一个函数FromTo(也不知叫什么名字更好),用来生成DataRange。请注意,我们的迭代器怎么实现,那都是细节。最后展示在用户层代码都是干干净净的function生成的DataRange,甚至连尖括号都不见了。也不用写具体是什么类型的DataRange,只须用auto让编译器自动推导类型就好了。
// step = 1是偷懒做法,万一Step的构造函数不能以1为参数就弱鸡了。比如DateTime和TimeSpan
template<typename Ty, typename Step>
auto FromTo(Ty from, Ty to, Step step 
= 1-> DataRange<ValueIncrementIterator<Ty, Step>>
{
    typedef ValueIncrementIterator
<Ty, Step> ValueType;
    
return DataRange<ValueType>(ValueType(from, step), ValueType(to, step));
}
于是,FromTo(1, 10, 2)就表示10以内的所有奇数,可以用for range的语法糖打印出来。
      这里的FromTo是按照上升状态产生一系列数据,同样,也可以产生下降的一堆数据FromDownTo,如果愿意的话,同学们也可以用迭代器形式生成斐波那契数列。不知注意到了,请用抽象的角度理解++和*这两个操作符。++就是为新的数据做准备进入到下一个状态,根据情况,可以有不同方式,进入到下一个状态,比如上面的ValueIncrementIterator根据步长递增到新的数值,ValueDecrementIterator的++却是在做减法,甚至还可以做Filter操作;*就是取到数据,我们可以在*的时候,才生成一个新的数据,这里从某种意义上来讲,其实就是延迟求值;而!=判断结束条件的方式又多种多样。总之,凭着这三个抽象操作,花样百出,基本上已经能够覆盖所有的需求了。
      为了体现这种抽象的威力,让我们给DataRange增加一个函数Concate,用于将两堆数据串联成一堆数据。首先,定义一个游走于两堆数据的迭代器,当它走完第一堆数据,就进入第二堆数据。
//不知道有什么语法能推导迭代器的值类型,所以搞这个辅助函数。可能写成type_trait形式更好,就算偷懒吧
template<typename Iter>
auto GetIteratorValueType(Iter
* ptr) -> decltype(**ptr)
{
    
return **ptr;
}

template
<typename Iter1, typename Iter2>
struct ConcateIterator
{
    typedef ConcateIterator ThisType;
    typedef Iter1 Iter1Type;
    typedef Iter2 Iter2Type;
    
//typedef decltype(*mBegin1) ValueType;
    typedef decltype(GetIteratorValueType((Iter1Type*)nullptr)) ValueType;

    ThisType(Iter1Type begin1, Iter1Type end1, Iter2Type begin2)
        :mBegin1(begin1), mEnd1(end1), mBegin2(begin2), mInBegin2(
false){}

    ThisType(Iter1Type end1, Iter2Type begin2)    
//这里有些蹊跷,不过也没什么
        :mBegin1(end1), mEnd1(end1), mBegin2(begin2), mInBegin2(true){}

    
bool operator != (const ThisType& other) const
    {
        
if (!mInBegin2 && other.mInBegin2)
            
return true;
        
if (!mInBegin2 && !other.mInBegin2 && mBegin1 != other.mBegin1)
            
return true;
        
if (mInBegin2 && other.mInBegin2 && mBegin2 != other.mBegin2)
            
return true;
        
return false;
    }

    ValueType 
operator* () const
    {
        
return mInBegin2 ? (*mBegin2) : (*mBegin1);
    }

    ThisType
& operator++ ()
    {
        
if (mInBegin2)
        {
            
++mBegin2;
        }
        
else
        {
            
if (mBegin1 != mEnd1)
                
++mBegin1;
            
if (!(mBegin1 != mEnd1))
                mInBegin2 
= true;
        }
        
return *this;
    }

    Iter1Type mBegin1;
    Iter2Type mBegin2;
    Iter1Type mEnd1;
    
bool mInBegin2;
};

有了ConcateIterator,DataRange的Concate函数就很好办了。
    template<typename OtherRange>
    auto Concate(
const OtherRange& otherRange)
        
->DataRange<ConcateIterator<IteratorType, decltype(otherRange.begin())>>
    {
        typedef ConcateIterator 
< IteratorType, decltype(otherRange.begin())> ResultIter;
        
return DataRange<ResultIter>(
            ResultIter(mBegin, mEnd, otherRange.begin()), ResultIter(mEnd, otherRange.end()));
    }
然后,试试
    list<int> numList = { 10, 11, 12 };
    for (auto i : Range(vvv).Concate(FromTo(4, 10, 2)).Concate(numList))   //后面随便接容器
    {
        cout << i << endl;
    }
     这样,就把两堆数据串联在一块了,是不是很酷呢?用C++11写代码,很有行云流水的快感,又有函数式编程的风格。下期节目继续发挥,给DataRange加入Filter,Map,Replace等操作,都是将一个DataRange变换成另一个DataRange的操作,显然,这是一种组合子的设计方式,也是吸收了haskell和linq的设计思路。某种意义上讲,就是给迭代器设计一套dsl,通过.操作符自由组合其成员函数,达到用起来很爽的效果,目标就是仅仅通过几个正交成员函数的随意组合,可以在大多数情况下代替stl算法的鬼麻烦的写法。这种dsl的最大好处类似于linq,先处理的步骤写在最前面,避开了函数调用的层次毛病,最外层的函数反而写在顶层。其实迭代器这个话题要展开来说的话,很有不少内容,比如用stackless协程来伪装成迭代器,Foldl,Foldl1,Scan等。当然,真要用得爽,还要配合boost中lambda的语法,好比什么_1+30,_1%2,当然,那个也可以自己写,因为C++现在已经支持lambda了,所以,自己写boost lambda的时候,可以剪裁,取其精华,去其糟粕。如果,再弄一个支持arena内存批量释放又或者是Stack风格的allocator(线程相关),那么就更不会有任何心智负担了,内存的分配和释放飞快,这样的动多态的allocator写起来也很有意思,它可以根据不同情况表现不同行为,比如说多线程下,就会用到线程同步,单线程就无须同步,每个线程单独拥有一个allocator,根据用户需要,还能用栈式内存分配,也就是分配内存时只是修改指针而已,释放时就什么都不做了,最后通过析构函数,将此allocator的内存一次性释放。当拥有一个表现如此多样的allocator,stl用起来真是爽。

posted @ 2016-05-14 02:10 华夏之火 阅读(788) | 评论 (2)编辑 收藏

2016年5月11日

c++单元测试框架关键点记录成员函数地址

原则上,C++下最好的单元测试代码应该长成这样子,用起来才是最方便的
TEST_CLASS(className)
{
    
// 变量
    TEST_METHOD(fn1)
    {
        
// 
    }    
    TEST_METHOD(fn1)
    {
        
// 
    }
    
//
}
vczh大神的测试代码是这样子,这是最方便使用的形式,但因为是以测试方法为粒度,大括号里面就是一个函数体,所以显得功能上有些不足。
TEST_CASE(ThisIsATestCase)
{
TEST_ASSERT(1+1==2);
}
      当然,这里隐藏了很多宏的丑陋实现,但是,那又有什么要紧呢。好不好并不是在于用了什么东西,goto,多继承,宏,隐式类型转换,……,这些,如果能够显著地减少重复性相似性代码,还能带来类型安全,然后又其潜在的问题又在可控的范围之内,那么,又有什么理由拒绝呢。老朽一向认为,语言提供的语法糖功能要多多益善,越多越好,当然,必须像C++那样,不用它们的时候,就不会带来任何代价,那怕是一点点,就好像它们不存在,并且它们最好能正交互补。但是,你看看,cppunit,gtest的测试代码又是什么货色呢。
      据说cppunit里面用了很多模式,其架构什么的非常巧妙。反正使用起来这么麻烦,要做的重复事情太多了,这里写测试函数,那里注册测试函数,只能表示,慢走不送。gtest据说其架构也大有讲究,值得学习,用起来,也比cppunit方便,但是,看看TEST_F,什么SetUp,TearDown,各种鬼麻烦,谁用谁知道。一句话,我们其实只需要class粒度的测试代码,其他的一切问题就都是小case了。
      当然,class粒度的单元测试实现的难点在于收集要测试的成员函数。这里不能用虚函数。必须类似于mfc里面的消息映射成员函数表。也即是当写下TEST_METHOD(fn1),宏TEST_METHOD就要记录下来fn1的函数指针。后面跟着的一对大括号体是fn1的函数体,已经越出宏的控制范围了,所以只能在前面大做文章。下面是解决这个问题的思路。这个问题在C++03之前的版本,比较棘手。但是,所幸,C++11带来很多逆天的新功能,这个问题做起来就没那么难了。下面的思路省略其他各种次要的细节问题。
首先,我们定义一个空类和要测试的成员函数的形式。
struct EmptyClass{};
typedef void(EmptyClass::*TestMethodPtr)();
还有存放成员函数地址的链表节点
struct MethodNode
{
    MethodNode(MethodNode
*& head, TestMethodPtr method)
    {
        mNext 
= head;
        head 
= this;
        mMethod 
= method;
    }
    MethodNode
* mNext;
    TestMethodPtr mMethod;
};
还有提取成员函数地址的函数

template 
<class OutputClass, class InputClass>
union horrible_union{
    OutputClass 
out;
    InputClass 
in;
};

template 
<class OutputClass, class InputClass>
inline 
void union_cast(OutputClass& outconst InputClass input){
    horrible_union
<OutputClass, InputClass> u;
    static_assert(
sizeof(InputClass) == sizeof(u) && sizeof(InputClass) == sizeof(OutputClass), "out and in should be the same size");
    u.
in = input;
    
out = u.out;
}
template
<typename Ty>
TestMethodPtr GetTestMethod(
void(Ty::*testMethod)())
{
    TestMethodPtr methodPtr;
    union_cast(methodPtr, testMethod);
    
return methodPtr;
}
方法是每定义一个测试函数,在其上面就先定义一个链表节点变量,其构造函数记录测试函数地址,并把自身加入到链表中。但是,在此之前,我们将遭遇到编译器的抵触。比如
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

    TestMethodPtr mTestMethodfn1 
= GetTestMethod(&fn1);
    void fn1(){}
};
      vc下面,编译器报错 error C2276: “&”: 绑定成员函数表达式上的非法操作
      原来在就地初始化的时候,不能以这种方式获取到地址。然后,试试在TestCase里面的其他函数中,包括静态函数,就可以将取地址符号用到成员函数前面。
      这好像分明是编译器在故意刁难,不过,任何代码上的问题都可以通过引入中间层来予以解决。用内部类。
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

   
struct Innerfn1 : public MethodNode
    {
        Innerfn1(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn1))
        {
        }
    } mTestMethodfn1 
= this;
    
void fn1(){}

    
struct Innerfn2 : public MethodNode
    {
        Innerfn2(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn2))
        {
        }
    } mTestMethodfn2 
= this;
    
void fn2(){}
};
      有多少个测试方法,就动用多少种内部类。然后,一旦定义一个测试类的变量,那么这些内部类的构造函数就执行了,把测试方法串联在一块,逆序,也就是说最后定义测试方法反而跑到前面去了。这样子就自动记录下来所有的测试方法的地址。有了这些函数地址信息,后面怎么玩都可以。包括漂亮的测试结果显示,日志记录,甚至嵌入到vs的单元测试界面中,又或者是生成配置文件,各种花招,怎么方便就怎么玩。这个时候,可以拿来主义,把cppunit,gtest等的优点都吸收过来。
      是否觉得这还不够,好像有很多事情要做。比如说,测试方法逆序了,在同一个测试类的变量上执行这些测试方法,会不会就扰乱类的内部信息了,每次new一个测试类,所有的测试方法都要重复记录,内部类变量要占内存……。咳咳,这些都可以一一解决。这里只是用最简明的方式展示自动记录测试方法,产品级的写法肯定大有讲究了。
      可以看到上面的代码都是有意做成很相似的,这些都是准备给宏大展身手的。这些低级宏太容易编写了,任何经历mfc或者boost代码折磨的猿猴,都完全能够胜任,这就打住了。对了,这里的自动记录成员函数的宏手法,可以大量地使用到其他地方,比如说,自动生成消息映射表,比mfc的那一套要好一百倍,应用范围太广了。当初老朽以为就只能用于单元测试框架的编写上面,想不到其威力如此巨大,消息系统全靠它了。C++的每一项奇技淫巧和功能被发现后,其价值都难以估量,好像bs所说的,他老人家不会给c++增添一项特性,其应用范围一早就可以预料的。对付一个问题,C++有一百种解决方案,当然里面只有几种才最贴切问题领域,但是很多时候,我们往往只选择或者寻找到另外的那90多种,最后注定要悲剧。

posted @ 2016-05-11 18:01 华夏之火 阅读(957) | 评论 (0)编辑 收藏

2016年5月10日

消息发送杂谈

      最近在看MFC的代码,虽然这破玩意,老朽已经很熟悉了得不能再熟悉了,但是这些破代码自由其独有的吸引力,不说别的,单单理解起来就非常容易,比之什么boost代码容易看多了,单步调试什么的,都非常方便,堆栈上一查看,层次调用一目了然。一次又一次地虐这些曾经虐过老朽的代码,也是人生快事一件。平心而论,mfc的代码还是写得挺不错的,中规中矩,再加上过去九十年代之初,16位的windows系统,那个时候的面向对象的c++的风靡一时,完全采用标准c++,能做成这样,实属难能可贵,也在情理之内。并且,后面压上com之后,mfc也不崩盘,采用内嵌类实现的com的方式,也很有意思。然后,从mfc中也能学到不少windows gui的使用方式还有各种其他杂七杂八东西,虽然win32已经没落。但是里面的技术还是挺吸引人,可以消遣也不错。当然,对于新人,mfc不建议再碰了,mfc真是没饭吃的意思。你想想,一个gui框架,没有template可用的情况下,而逆天c++11的lambda作为匿名functor,更加不必提了,只有虚函数和继承可用,也没有exception,能搞成mfc这副摸样,的而且确是精品。其实,后来的巨硬也有救赎,看看人家用template做出来的专为com打造的atl又是什么样子呢,然后建构在atl的windows thunk基础上开发的wtl又是怎样的小巧玲珑。巨硬在template上的使用还是很厉害的,atl将template和多继承用的真是漂亮。人家几十年前就将template和多继承用得如此出神入化,反观国内,一大批C with class又或者狗粉一再叫嚣template滚出c++,多继承太复杂了,运算符重载不透明,心智负担,隐式类型转换问题太多,virtual是罪恶之源万恶之首,构造函数析构函数背着马猿做了太多事情,exception对代码冲击太大,打断代码正常流行,时时刻刻都好像隐藏着不定时炸弹。依本座看来,C++中一大批能够显著减少重复代码,带来类型安全的拔高抽象层次的好东西,对于这些C语言卫道士而言,都是混乱之物。其实,c语言就是一块废柴,抽象层次太低,可以做文章的地方太少了。
      就以构造函数和类型转换operator为例,来看看怎么用于C的char *itoa(int value,  char *str,  int radix)。
      itoa的参数之所以还需要str入参,那是因为C语言中缺乏返回数组的语言元素,所以调用者要提供一个字符数组作为缓冲用于存放结果,但是这个多出来str参数真是没必要啊,因为语言能力的欠缺,所以只好把这个负担压到猿猴身上。也有些itoa的实现没有这个累赘str的入参,而是内部static的字符数组,用于存放结果并返回给上层。这个版本就只有两个入参了,但是也太不安全了,别提多线程了。假如,有一个函数fn(char* str1, char* str2),然后,这样调用fn(itoa(num1),itoa(num2)),画面太美了。另外,那个有多余str参数版本的itoa也好不到哪里去,要劳心费神准备两块字符数组,然后还要保证参数传递的时候不要一样。反正C语言的粉丝整天很喜欢写这些重复代码,并且美其名曰掌控一切细节的快感。
请看构造函数和类型转换operator怎么解决。太easy了。

struct ToString
{
    
char text[28];
    
int length;

    ToString(
int n)
    {
        
//转换字符串,结果存放于text中
    }

    
operator const char*()
    {
        
return text;
    }
};
      并且,这里的ToString还可以安全的用之于printf里面呢,因为它本身就是字符串的化身。为什么是ToString,因为它不仅仅要它ToString int,还有double,bool,char,……
      不好意思,扯远了,只是想说,框架或者函数库的表现能力也要取决于语言本身的表达能力。就好像C#就可以做出linq那样爽快的框架,java再怎么拼命也捣鼓不出来一个一半好用的linq,C++也不行,但是C++可以捣鼓类似于haskell中的map,filter,fold等,  并结合linq的后缀表达方式。就好比下面这样
      vector<int> nums = {...}
      Range(nums).Map(_1 * _1).Filter(_1 % 2).CopyTo(dest); // 用了boost中的lambda表达法,因为的确很简洁,没法控制。对于复杂情况,当然要用C++11原生的lambda
      勉勉强强差可满足吧。如果C++的lambda参数可以自动推导就好了,不过也没什么,主要是ide下用得爽。用泛型lambda也能将就。
      所以,回过头来,再看看mfc(没饭吃),就可以了解其各种隐痛了。真的,以90年代的眼光来看,mfc真是做到极致了。mfc不可能走win32下窗口函数C语言那样的消息发送消息反应的正路(邪路)吧。窗口函数这一套,在90年代面向对象盛行的时代,绝对不能被忍受,只是到了前几年,才被发现其价值原来不菲,这是解耦合砍继承树的好手法,老朽在前几年也跟风吹捧窗口函数的那一套。平心而论,smalltalk的这一套消息发送的动态语言,确实是很强有力的抽象手段,我不管目标对象能否反应该消息,闭着眼睛就可以给你发送消息,你能反应就反应,不能反应就拉倒,或者调用缺省的反应方式,就好像DefWindowProc(职责链模式?),又或者是抛出异常,怎么做都可以。一下子就解开了调用者和目标对象的类型耦合关系。面向对象中,消息发送和消息反应才是核心,什么封装继承多态,那是另一套抽象方式,虽然坊间说这也是面向对象的基本要素,但是不是,当然,这或许也只是个人观点。
      或许,从某种意义上讲,C++/java/C#一类的成员函数调用形式,其实也算消息发送吧。比如,str.length(),就是给对象str发送length的消息,然后str收到length消息,作出反应,执行操作,返回里面字符串的长度。靠,这明明就是直接的函数调用,搞什么消息发送的说辞来强辩,颠倒是非黑白,指鹿为马。可不是吗?编译器知道str是字符串类型,知道length成员函数,马上就生成了高效的函数调用方式。在这里,没有任何动态多态的可能,发生就发生了,一经调用,动作立马就行动,没有任何商量的余地。耦合,这里出现强耦合,调用者和str绑在一块了,假如以后出现更高效率更有弹性的string的代替品了,可是没法用在这里了,因为这里str.length()的绑定很紧很紧。
      人家消息发送就不一样了,动态的,可以动态替换对象,替换方法,弹性足足。并且,消息发送的模式下,对象收到消息,要判断消息,解析消息,找到消息的执行函数,最后才终于执行任务。这么多间接层,每一层都可以做很多很多文章。比如,在消息到达对象之前做文章,就可以搞消息队列,把消息和参数暂存起来,这个时候,什么actor模式就大放异彩,至于undo,redo,更加是小菜一碟。然后呢,再给对象安装消息解析器,把消息和消息参数转换成其他类型消息。比如原本对象不能反应这条消息,但是对消息参数稍加修饰,然后在发送给对象,这不就是适配器模式。总之,可操作可挖掘的空间太大了,远远不止23条。
      但是,封装继承多态就一无是处了吗?不是的,最起码一点,编译期间可以报错。因为的确有很多时候,我们明明就知道对象的类型,明明就知道对象不可能是其他类型,比如字符串,比如复数,比如数组这些玩意,无论如何,它们都不需要动态消息的能力。我们就知道手上的对象就是字符串就是复数,不可能是别的,并且,我们就是要明确地调用length函数。我们就是要编译器帮忙检查这里潜在的语法类型错误,比如对复数对象调用length函数,编译器马上就不高兴了。并且,一切都是确定的,所以编译器也能生成高效的代码,高效的不能再高效了。对此,消息发送的面向对象就做不到了,不管是什么对象,int,string,complex种种,都来个消息发送。这样一来,静态类型检查和高效的代码,就木有了。
考察一下,面向对象有等级之分,一步一步,有进化的阶梯。每进化一次,就多了一层间接,类型耦合就降低,就进一步超越编译器的限制,当然,也意味着编译器帮忙检查类型错误生成高效代码就弱了一分。事情往往就是,有所得必有所失。少即是多,多即是少。因此,可推得少即是少,多即是多。少始终是少,多始终是多。
      一切,还是要从C语言说起,C语言中,没有class,没有函数重载。函数名是什么,最后就是什么。在这种条件下,代码多了,每个新的函数名字要考究半天,一不小心,要么函数名字就会很长,要么函数名字短了要冲突或者不好理解。但是好处是,最后生成目标代码时,什么函数名字就是什么名字,所见即所得,没有异常,不会捣鬼,于是其他各种语言都可以高高兴兴开开心心调用。猿猴观码,也很清晰。C++也是在这里赚了第一桶金。其实,这么苛刻的条件下,最考究猿猴的代码架构能力,架构稍微不好,最后都势必提早崩掉,前期就可以过滤很多垃圾架构。
      然后就是C with class了,开始在函数名字上面做文章了。同一个函数名字依对象类型,开始拥有静态多态能力了。比如,str.length(),这是对字符串求长度。数组的变量,nums.length(),对数组求长度。同一个length的名字,用在不同的对象上,就有不同的意义。这如何做到呢,最初,cfront(第一版C++编译器)的处理方式是,可以说是语法糖,就是在名字和调用形式上做文章,比如,str.length(),变成,string_length(&str),array_length(&nums)。别小看这点小把戏语法糖,这真是有力的抽象手法。不说别的,就说起名字吧,可以统一length了,无须费思量string_length,list_length了。然后,对象统一调用方式,str.length(),list.length(),函数确定这种吃力不讨好的事情就交给编译器去做好啦,解放部分脑细胞。这,的确很好,但是,全局函数是开放式的,而对象的成员函数是封闭的,一旦class定义完毕,成员函数的数量也就定死了。猿猴最讲究东西的可扩展性,不管成员函数多么方便多么抽象有力,就扩展性而言,就差了一大截,其他优势都弥补不了。语义上看,扩展成员函数的语法完全与原生的一样,增加一个简单的语法形式来扩充,但是多年下来,标准委员会都不务正业,哎。显然,编译器的类型检查能力和生成的代码性能,没有任何减少,但是,猿猴看代码,不能再所见所得了,必须根据对象类型,才能确定最终的目标函数。就这么点小改进,当时C++马上就展示其惊人的吸引力。假如,C++只能留在这一层,相信到今天为止,可以吸引到更多的c粉。可是,C++开始叛变。
      C++的函数重载,还有操作符重载,外加隐式类型转换和隐式构造函数,还有const,volatile修饰,当然,代码可以写得更加简洁,编译器可以做的事情也更多啦,但是函数的调用再也不明确了。部分专注于底层的猿猴的弱小的抽象能力把控不住了,不少人在这里玩不动了。此外,命名修饰把最终函数名字搞得乱七八糟,二进制的通用性也要开始废了。导致C++的dll不能像C那样到处通吃。像是狗语言就禁止函数重载这个功能。大家好像很非难C++的操作符重载,但是haskell还能自定义新的操作符呢。虽然在这里,编译器还能生成高效代码,但是,各种奇奇怪怪类型转换的规则,编译器也偶尔表现出奇,甚至匪夷所思,虽然一切都在情理之内。
      其实,不考虑什么动态能力,单单是这里的静多态,基于对象(俗称ADT)的抽象模式,就可以应付70%以上的代码了。想想以前没有静多态的C日子是怎么过的。
      此时,开始兵分两路,C++一方面是动多态发展,表现为继承,多继承,虚继承,虚函数,纯虚函数,rtti(废物,半残品),到此为止了,止步不前;另一方面是继续加强静多态,王者,template,一直在加强,模板偏特化,template template,varidiac tempalte,consexpr, auto,concept,……,背负着各种指责在前进,就是在前进。C++企图以静态能力的强悍变态恐怖,不惜榨干静态上的一点点可为空间,累死编译器,罔顾边际效应的越来越少,企图弥补其动态上的种种不足。这也是可行的,毕竟haskell都可以做到。template的话题太庞大了,我们言归正传,面向对象。
      下面就是被指责得太多的C++多继承,虚函数,RTTI,脆弱的虚函数表,等,这些说法,也都很有道理,确是实情,兼之C++没有反射,没有垃圾回收,用上面这些破玩意捣鼓,硬着头皮做设计做框架,本来就先天能力严重不足,还要考虑内存管理这个大敌(循环引用可不是吹的),更有exception在旁虎视眈眈,随时给予致命一击。更要命的是,多继承,虚函数,虚继承,这些本来就杀敌八百自伤一千,严重扰乱class的内存布局,你知道vector里面随随便便插入元素,对于非pod的元素,不仅仅是移动内存,腾出新位置来给新对象安营扎寨,还要一次又一次地对被移动的对象执行析构拷贝构造。没有这些奇奇怪怪的内存布局,vector的实现应该会清爽很多。稍微想想,这实在太考究猿猴的设计能力,其难度不亚于没有任何多态特性的C语言了。可以这么说,继承树一旦出现虚继承这个怪胎,整体架构就有大问题,毫无疑问,iostream也不例外。不过,如果没有那么多的动态要求,好比gui框架的变态需求,严格以接口作为耦合对象,辅以function,也即是委托,又可以应付多起码15%的局面。其实,必须要用到virtual函数的时候,将virtual函数hi起来,那种感觉非常清爽,很多人谈virtual色变,大可不必。C#和java还加上垃圾回收和反射,这个比例可以放大很多。在这种层次下,接口最大的问题是,就好像成员函数,是封闭的。一个class定义完毕,其能支持的interface的数量也就定死了,不能再有任何修改。interface可以说是一个class的对外的开放功能,现实世界中,一种东西的对外功能并不是一开始就定死了的,其功能也在后来慢慢挖掘。但是,C++/java/C#的接口就不是这样,class定义完毕,就没有任何潜力可言了。明明看到某些class的能力可以实现某些接口,甚至函数签名都一样,对不起,谁让你当初不实现这个接口。对此,各种动态语言闪亮登场,或mixing或鸭子类型。接口还有另一尴尬之处,比如,鸟实现了会飞的接口,鸭子企鹅也继承了鸟,自然也就继承了会飞的接口,没办法不继承。面对着一个需要IFlyable参数的函数,我们顺利的传一只企鹅进去,然后企鹅再里面始终飞不起来,就算企鹅在被要求飞的时候,抛出异常,也不过自欺欺人。这种悲剧,就好像有些人很会装逼,最后一定会坏事。搞出接口这种破事,就是为了让编译器做类型检查的。又有人说,bird应当分为两类,会飞的和不会飞的,这的确能解决飞行的尴尬。但是,有很多鸟具备捉虫虫的能力,然后又有那么一小撮鸟不会捉虫只会捉鱼,难道又要依据捉虫能力再划分出鸟类。于是鸟类的继承树越长越高,画面越来越美。这分明就是语言能力的不足,把问题交给猿猴了。请谨记,接口就是一个强有力的契约,既然实现了一个接口,就说明有能力做好相关的事情。再说,既然interface这么重要,于是我们再设计class的时候,就自然而然把精力放在interface这个对外交流媒介的手段之上了,而忽视了class本身的推敲。class最重要的事情就是全心全意做好独立完整最小化的事情,其他什么对外交互不要理会。一个class如果能够完整的封装一个清晰的概念,后面不管怎么重构,都可以保留下来。但是,interface会分散这种设计。接口的悲剧就在于企图顶多以90分的能力去干一百分的事情,并且还以为自己可以做得好,硬上强干,罔顾自身的极限。往往做了90%的工作量,事情恰恰就坏在剩下来的10%上。
      于是,狗语言走上另一条邪路,鸭子类型。只要class,不,是struct,这种独特关键字的品味,只要某个struct能够完全实现某个interface的所有函数,就默认其实现了这个接口。并且,狗语言还禁止了继承,代之以“组合”这个高大上的名词了,但是,细究一下语义和内存布局(忽略虚函数表指针),你妈的,不就是一个没有virtual继承的弱多继承吗?显式的继承消失了,隐式的继承还存在的,好了,还不让你画出继承树关系图,高高兴兴对外宣称没有继承了,没有继承并不表示继承的问题木有存在。但是,因为狗语言的成员函数方法可以定义在class,不,struct外面,其扩展性就非常好了,对于一个interface,有哪些方法,本struct不存在,就地给它定义出来,然后,struct就轻松的实现了该接口,即使原来的struct不支持该接口,以后也有办法让它支持,很好很强大。之所以能做到这一点,那是因为狗语言的虚函数表是动态生成的。小心的使用接口各种名字,部分人应该狗语言用起来会相当愉快。可是,你妈,不同接口的函数名字不能一样啊,或者说,同一个函数的名字不能出现在不同接口中。不过,这个问题并不难,不就是不一样的名字吗,c语言中此等大风大浪猿猴谁没有见识过。对于狗语言,不想做太多评断,只是,其扩展性确实不错,非侵入式的成员函数和非侵入式的接口,理应能更好地应付接口实现这种多态方式,只是,编译器在上面所做的类型约束想必会不如后者,重构什么的,想必不会很方便。自由上去了,约束自然也下来了。听起来挺美,但是内里也有些地方要推敲,反正老朽不喜欢,以后也不大会用上,当然,给money自然会用,给money不搞c++都没问题。老朽还是比较喜欢虚函数的接口,更何况c++通过奇技淫巧也能非侵入式的给class添加接口。在静态语言中搞这种鸭子类型的动态语言接口,显得有点不伦不类。
      然后就是com接口的面向对象,完全舍去编译器对接口类型的约束,自然能换来更大的自由。由于com的语言通用性目标,所以搞得有点复杂,但是com背后的理念也挺纯洁。老朽猜测com好似是要在静态语言上搭建出一个类似于动态语言的运行平台,外加语言通用性。其契约很明确,操作对象前时,必须先查询到对象支持的接口,进而调用接口的函数。这里有意思的地方在于面对着一个com对象,你居然没有办法知道到它究竟实现了多少接口。
      最后就是消息发送了,其能力之强大,谁用谁知道。原则上讲,可以看成对象拥有的虚函数表的方法无穷多,又可以把每一条消息看成一个接口,那么,对象可能就实现了无穷多的接口。你说,面对着这样对象,还有什么做不出来呢。真用上消息发送这种隐藏无数间接层,就没有什么软件问题解决不了的。任何软件问题不就是通过引入间接层来解决的嘛。现在用上消息发送这种怪物,就问你怕不怕。没有免费午餐,自然要付出类型安全的危险和性能上的损失。

posted @ 2016-05-10 22:40 华夏之火 阅读(1222) | 评论 (3)编辑 收藏

2016年5月9日

挖坑,有空填坑


先挖坑,计划写出一系列文章,探讨将c++用成动态语言,或者函数式语言,以达到快速开发的目的,并且在需要优化的情况下,又能够方便快速的优化。现在事务太多,不知道何时能填坑

宏的图灵完备,用宏生成代码,特别是反射,模式匹配,实在必不可少,以至于宏可以与c++的继承、template、exception等基本组件并列的重要必不可少的补充手段

最小巧方便使用的单元测试框架,比gtest,cppunit要好用很多

自定义内存管理器,stl中的allocator是作为模板参数来传递,尝试以tls来传递allocator参数,当然,必须相应的各种容器都要重写,修改其缺省构造函数,拷贝复制移动拷贝,给元素分配内存释放内存等。对了,还有各种容器的反射信息。每种类型的template的容器都有一个typeinfo对象,具体的容器又有自己独一的typeinfo对象

完善完备的reflection,也就是,其他language能够做的反射的事情,这里只要愿意,也可以做到,非侵入式,可以给int,double等基本类型添加反射,给template类型的也添加反射信息,保证每种类型的反射对象是唯一的;

史上功能最完善的fmt的实现,非template,当然,外层还需要variadic来包装,以类型信息。类型安全,缓冲安全,高效,通用。通用的意思是,可以fmt到文件,日志,字符串,文本框控件中;类型安全的意思是,可以是所有的类型都可以fmt,只要该类型实现了相应的接口,但是,这种接口是非侵入式的,通过模板特化。高效的意思是合sprintf系列一样。调用的时候如下:
fmt(text, "%s %s %d ", 20, 17.5, 'a'); //故意写错%s的,在这里,%s为通用符号
fmt(file, "{%s-}",{1, 2, 3}); //输出 1-2-3到文件中,也即是能够fmt容器对象,横线-为容器对元素的分隔符

带有切片功能的数组,此数组类型还支持子类型数组到基类型数组的隐式转换,也即是需要用到基类型数组的参数,子类型数组都可以适应

haskell的map,filter,fold算法在C++下的方便灵活组合性的改造,使用时,就好像C#的linq那么爽快,当然,没有lambda的参数自动推导,毕竟还不如

stackless协程

c++下的monad

wpf的依赖属性在c++下的实现,gui框架的不可缺少的要素

tupple的功能扩展,通过宏,不需要写类型,用起来就好像函数式语言原生的那么爽的可能

好像haskell或者f#那样的模式匹配的结构体

C++下完完全全实现狗语言的那种鸭子类型的接口

面向对象的深入探讨,对于企鹅或者鸡是一种鸟,继承了鸟,但是没有继承了会飞的接口,在编译期就能报错,在运行期也不能对其找到会飞的接口

具体类,基本类型,没有虚函数,但是又能实现接口的方式,是实实在在的接口,里面有纯虚函数,也即是非侵入式的实现接口,上面宇宙最强悍的fmt就是用到这里的技术

vistor模式和抽象工厂的解耦合,或者又叫,multi dispatch

类型安全的消息,一条消息就代表了一种函数调用,不是win32的那种一点也不安全的类型系统,然后可以向任何类发送消息,动态添加消息的反应,消息队列,消息和消息参数的保存,actor,command模式,redo或undo的轻松实现,消息广播

空基类优化的运用,除了多继承(ATL)或者内嵌类(MFC),还有其他方式,那是以组合方式,通过少量的模板和少量的宏,通过搭配组装(多继承空基类)各种基类,就能完成一个com组件

消息系统的构建,gui框架的编写

........

博大精深的c++!只是想说,上面的一切,在C++下全部都是可行的,当然,宏,template,多继承必须大用特用,只是,奇妙的是,主类的内存布局却很干净,甚至可以没有虚函数
不知道有生之年能否填完坑,以之为励吧!
c++的同学们也充分发挥想象力吧,太多的奇技淫巧了。

posted @ 2016-05-09 20:36 华夏之火 阅读(728) | 评论 (8)编辑 收藏

stl中string的一种改造

      stl中最难看的组件(没有之一),无疑就是string这货了,一百多个成员函数,当然里面大多数是重载的,不必多想,一个class,如果拥有如此之多的函数,必然一定肯定是失败的,并且,即便是这么一大打函数,string的功能还是很不完备,要不然,就不会有boost里面的string算法。这真是尴尬,string作为最基本最基本的语言组件,又出自官方标准库,长成这样子,真是让无数的c++粉丝要失望,失望归失望,毕竟师出iso,用起来还是很有保障的,论性能什么,再怎样,也不会亏到那里去。只是,很让人好奇的是,这成百个函数又功能不完备的string,里面都有些什么货色,对此,c++exception系列中有过分析。但是,在此,想探讨一下,除了小胡子的方法之外,用其他方法压缩string的成员函数的数量。
      我们先来看看string的append成员函数,怪怪龙的东,总共有8个重载之多,好像还不止,突然想起狗语言的名言,少即是多,反过来说,多即是少。
basic_string<CharType, Traits, Allocator>& append(
     
const value_type* _Ptr
);
basic_string
<CharType, Traits, Allocator>& append(
     
const value_type* _Ptr,
     size_type _Count
);
basic_string
<CharType, Traits, Allocator>& append(
     
const basic_string<CharType, Traits, Allocator>& _Str,
     size_type _Off,
      size_type _Count
);
basic_string
<CharType, Traits, Allocator>& append(
     
const basic_string<CharType, Traits, Allocator>& _Str
);
basic_string
<CharType, Traits, Allocator>& append(
     size_type _Count, 
     value_type _Ch
);
template
<class InputIterator>
     basic_string
<CharType, Traits, Allocator>& append(
         InputIterator _First, 
           InputIterator _Last
      );
basic_string
<CharType, Traits, Allocator>& append(
        const_pointer _First,
       const_pointer _Last
);
basic_string
<CharType, Traits, Allocator>& append(
        const_iterator _First,
       const_iterator _Last
);
      这么多的重载,其实可分为两类,一类是迭代器版本的append,对于插入n个相同的字符append,可以看做是特殊迭代器。另一类是连续字节内存块的append。这里,只关注后一类。虽然有4个之多,但其实只需要一个就行了,那就是 append(const basic_string<CharType, Traits, Allocator>& _Str)。因为字符指针可以隐式转换为string,另外的两个重载可以临时构造string,然后传递进append就好了。之所以存在4个,老朽的猜想可能是因为效率,至于调用上的方便性,并没有带来多少提高。string的其他类似于用append的通过参数来string的操作,如replace,insert,+=,那么多的重载版本,应该也是同样的原因。
      假如,临时string对象的构造没有造成任何性能上的损失,那么,应该就可以减少几十个成员函数,这无疑很值得尝试。那么,能否存在廉价的string临时构造方法,因为它知道自己是临时对象,只作为临时参数传递的使命,不会在其上面作什么赋值,添加,修改等操作,也就是说,它是不可变的,那么,这个临时string对象就不需要分配内存了,只要节用ptr作为自己字符串的起始地址,然后以长度作为自己的长度。参数传递使命完成后,也不需要销毁内存了。
      可是,C++中,也不仅仅是C++,所有的语言并没有这样的机制来判断对象它在构造的时候,就是仅仅作为参数传递来使用的。为了达到这种目的,很多时候还不惜使用引用计数,但是,很多场合,临时string对象始终要构造缓冲存放字符串,比如这里。
除了C++,任何语言的字符串都是不可变的,任何对于字符串的修改,都意味着要创建另一个全新的字符串来,那怕仅仅是修改了一个字符。其实,不可变的字符串,在C++中运用很广的,很多时候,我们仅仅只需要不可变的字符串,比如说,这里的append,全部只需要immutable的string。只要知道string是immutable的,那么,c++完全可以高效的应付,既然是immutable,就不需要考虑什么资源分配释放的龟毛问题了。下面,就尝试class一个immutable的字符串,这,太容易了。就是:
struct Str
{
    typedef 
const char* PCStr;
    PCStr start;
    size_t length;
    Str(PCStr text, size_t len)
    {
        start 
= text;
        length 
= len;
    }
    
//
};
      然后,在basic_string中加入operator Str的函数,以完成从一个string到一个Str的隐式转换,这个隐式转换简直没有任何性能上的损失。还有,string中再增加一个Sub的成员函数,用于截取一段子字符串,也即是immutable的Str对象。显然,我们的Str其实表达了一个概念,内存中一节连续的字符内存,也即是数组。
      最后,append就变成append(Str str);了。Str加不加const,或者Str是否为引用,关系都不大。下面,看看它的运作。
对于,append(const char* text),由于Str中有一个const char*参数的构造函数,text自动隐式转换为一个Str,很好;
对于,append(const char* text,size_t count),用append(Str(text, count)),就地构造一个临时的Str对象,嗯,语法调用上多了一个Str和一对括号,多了5个字符,的确有点不便。
对于,append(const string& text),同上,string中有一个operator Str的函数,隐式转换自动完成。
对于,append(const string& text,size_t offset,size_t count),用append(text.Sub(offse, count)),就地构造一个临时的Str对象,嗯,语法调用上多了一个Sub和一对括号和一个点,但是少了一个逗号,多了5个字符,有点不便。
      即此以推,string中的replace,insert,assign,+=,=等函数,每个平均减少3个,总共差不多可以减少20个左右啦,而功能上没有任何减少,可喜可贺。
      然后,string中的各种查找比较操作的const的成员函数,比如find,find_first_not_of,rfind等,都可以挪到Str旗下了。因为这些函数,我们也希望可以用之于其他地方,只要那是一块连续的字符内存好比数组,那么我们就可以就地快速构造一个临时Str对象,进行find,rfind这些操作了。当然,原来string也可以有这个功能,但是想到仅仅为了做一个find或者find_first_not_of的查找,就要分配内存释放内存,对于性能优先的巴普洛夫反应的C++猿猴来说,这绝对是望而生畏的大事。现在通过不可变的Str,马上就释放出来string的成员函数的隐含的生产力了。 由于Str的廉价和透明性,就可以到处乱使用,想用就用,何其快哉。
      原来string没有了这些查找的函数,每次要用它们,必须转换这样调用,((Str)text).find,无疑很不方便,对此,我们只要在string中再增加一个Str的成员函数,以返回临时Str对象,就可以text.Str().find(),似乎有点不便,但也不是不能接受。
当然,Str也有缺点,那就是它不以0结束,导致很多对于要求以0结束的地方,就变成禁区了,这坑爹的C语言规定。
      这不是很明显吗?字符串的一部分也是字符串,随便取出字符串的一节,本来就应该是字符串,这么简明统一简洁明显的概念,这样可以简化多少代码呢,结果,偏偏只有带有0结束的那一节字符串,才是C语言承认的字符串。一个很好的概念,就这样在很多地方失去用武之地了。你因为以0结束的字符串很好吗,要不cstring头文件中也不会有那么多带有字符串长度版本的字符函数,如strncpy,来补充了。
      对了,有没有觉得string中的find_last_of,find_first_of,find_last_not_of,find_first_not_of很碍眼啊,显然这是一种不用组合思想下设计出来的api产物了。其实,别看stl是官方iso的嫡出亲子,但是,内中的很多api的设计都不咋样,实在不是学习的好对象。你还别不服,想想人家C#linq的链式调用,那个用起来,才叫痛快。

posted @ 2016-05-09 19:28 华夏之火 阅读(896) | 评论 (2)编辑 收藏

2013年7月11日

scheme下的停机问题和Y组合子

        看过的计算机书中,scheme相关的那几本,好比SICP,the essence of program都很让我爱不释手。而the little schemer更加独特,编程的本质,在这本书小人书上体现得淋漓尽致。窃以为,scheme是语法形式上最为完美的编程语言了,没有之一。少即是多,这样的赞美之言,唯有scheme当之无愧,并且它的确是精简得不能再精简了。至于那个自吹自擂的什么狗语言,不提也罢。当然,完美并不一定代表实用,也并不一定必须流行,曲高一向都是和寡的,但是,完美却一定可以带来赏心悦目般的感受。
        the little schemer全书行云流水,逐渐显露递归的威力,做足了铺垫,到了第8章,真命天子lambda出现,一切变得很有意思了,读完之后,意犹未尽。第9章,难度陡增,突然变得理论性起来,那是自然的。因为,这一章的主题是停机问题和Y组合算子。不引入任何形式化的方法,但是作者举重若轻,依然阐释得如此直白易懂,可以说,只要能看完前面的内容,就一定能看懂这一章。而前八章,据说6岁以上的儿童都能看得明白。
        scheme中的函数是first class,可以作为参数传递给其他函数,也可以作为值从函数中返回。比如,广为流传的一道程序,用以考察语言的表达能力,“编写一个函数,其入参数为n,返回值为新的函数,该函数的参数为x,返回值为之前的n与现在的x的和。”用scheme来表达,牛刀小试。
(define (addn n)
  (lambda (x)
    (+ x n)))
然后,对于((addn 20) 30),scheme解释器上显示其结果为50,很好。相比于那个lisp的版本,这里显得多么的干净。
        函数式的语言对副作用(side effect)很敏感,特别是haskell,更加对副作用赶尽杀绝,压根就不让写出有副作用的函数。因此,正常情况下,函数执行完毕,都有返回值。好比,……,总之很多就是,正常的函数,都可称之为total functions,意思就是对所有的参数,都会有返回结果。但是,也还存在一些病态函数,它们不会返回,一旦调用它,那么将陷入与其中,永远都不会返回了,显然,里面出现死循环了,但是,scheme中没有循环语句,所以不能这么说,总之,这一类是不会有返回值的。很轻易就能写出一个例子。
(define eternity
  (lambda (x)
    (eternity x)))
eternity为不朽的意思。
        自然就有这样的问题,能否实现这样的函数,它能判断函数是否终将返回,或者说,判断函数会不会停止。这个函数作用可大了,当然不会那么容易实现。不过,可以先假设它存在,就叫它will-stop?(别惊讶,scheme中,标识符中可以有+-*等特殊符号)。因此,对于任何函数,(will-stop? afunction)表达式的值,要么为#t,表示函数afunction终将停止返回;要么为#f,则函数不会停止,好比eternity。显然,让will-stop?判断自己,(will-stop? will-stop?)的结果一定是#t了。
        但是,will-stop?是不可能存在的,这不是废话吗,地球人都知道。因为计算机学家精心构造了一个反例,此反例实在巧妙,真难以想象当初是如何构造出来,我等小民只需理解即可。请看代码
(define (last-try x)
  (and (will-stop? last-try) (eternity x)))
last-try,好名字,就叫它最后一击吧。(will-stop? last-try)的结果不外乎#t或#f。
        假如为#f,说明last-try不会返回,意味着有死循环,不会停止。但是,一考察last-try的内部实现,却很容易就知道它马上就返回了。表达式(and (will-stop? last-try) (eternity '()))中,由假设可知(will-stop? last-try)为#f,进而马上可知,(and (will-stop? last-try) (eternity '()))马上必将返回#f,也就是说,虽然一开始假设last-try不会停止,但实际运行中last-try一下子就返回了,矛盾。
        看样子,(will-stop? last-try)只好为#t了。可是,(and (will-stop? last-try) (eternity '())),and表达式的两个分支中,既然(will-stop? last-try)为#t,那么,势必要进一步调用(eternity '()),而eternity老爷,一早就知道他乃不朽之身了,因此,last-try也沾光,一样不朽了。与假设中(will-stop? last-try)为#t为终将停止,又是矛盾。
        因此,will-stop?接受不了last-try的挑战,失败。也就是说,will-stop?这样的函数,不存在。这道反例的高明之处,或者说耍赖吧,就是以will-stop?为基础构造了一个will-stop?无法判断的函数。假如规定,所有被检测函数都不得直接间接的调用will-stop?,免得will-stop?难堪,那么这样的will-stop?能否存在呢?存不存在,我就不知道了,但享受此待遇的Y组合子却是存在的。
        函数直接或间接调用到它自己,递归就产生了。问题来了,函数你自己都还没实现完毕,怎么就可以自己拿来调用呢?这个过程中,编译器解释器肯定做了某些语义上处理,让递归得以实现。逻辑学中,对于下定义的要求是“不得循环”,好比,白色就是一种白色的颜色,这种废话定义就不符合下定义的基本要求了。
        下面来将一条经典的递归函数整成非递归的版本。the little schemer的推导思路非常浅显易懂,我不能做的更好的了,因此借用。
(define length
  (lambda (l)
    (cond ((null? l) 0)
      (else (+ 1 (length (cdr l)))))))
函数length中,虽然调用到了自己,实际上,其实只是调用了一个同样名字的函数而已。意味着,length的实际上的lambda表达式,背地里带多了一个参数,此参数为函数,用以当入参l不为空时来进行使用。因此,可以将整个函数的定义改写成下面的lambda表达式。
(lambda (length)
  (lambda (l)
    (cond ((null? l) 0)
      (else (+ 1 (length (cdr l)))))))
lambda表达式的返回值为一个函数,当然没有名字了。它的入参为一函数,返回一个新的函数,此新函数的入参是列表,返回列表的长度。为了便于后文叙述引用,就用define给它起个名字,叫mk-length。什么,连用define起名字都不会,没救了。
        mk-length不是需要函数入参吗?刚好手头有一个,就用它自己本身,((mk-length mk-length) '()),解释器返回0,太好了。然后,我满怀希望的用((mk-length mk-length) '(a))来测试,结果,解释器报错了,为什么?稍微一想,就明白了。(mk-length mk-length)的确返回计算列表长度的函数,但是,当列表不为空时,只好用表达式(+ 1 (length (cdr l)))做进一步处理,里面的length就是mk-length,而mk-length的入参是函数,不是列表,于是解释器就报错了。怎么办?
        当然,要计算长度为不大于1的列表的长度,还是有办法的。就是,((mk-length (mk-length mk-length)) '(a)),这样就好了。自然,当列表大于1时,解释器必然又将报错了。按照此法,为此,为了求得不大于N个元素的列表长度,必须将mk-length写N次,好比,
((mk-length
  (mk-length
   (mk-length (...))))
 '(a b c d ...))
并且,辛辛苦苦的重复写N遍mk-length,只能计算个数不大于N的列表的长度。这,无论如何都不能让程序猿接受。
那么,为何要写那么多(mk-length (mk-length (mk-length...))),皆因mk-length中(+ 1 (length (cdr l)))的length函数接收的函数参数是列表l。先暂时让它适应环境,就让它知道它接收的length参数是一个跟它自己本身的lambda表达一样,是入参为函数,然后返回一个计算list长度的函数。将mk-length改写成这样。
(define mk-length
  (lambda (length)
    (lambda (l)
      (cond ((null? l) 0)
        (else (+ 1 ((length length) (cdr l))))))))
请注意,代码里面已经不存在递归形式了,因为,mk-length的lambda表达式中,没有用到mk-length这个名字了,当然,它还要用到入参length以计算当l不为空时的长度。再次抱着试试看的态度,验证,((mk-length mk-length) '(a)),返回1,真的可以了。拿更长的列表丢进去,长度为2,为3,为N+1,都OK了,真是神奇。
        它的工作原理是,故事一开始,(mk-length mk-length)生成一个计算列表长度的函数,在其内部中,假如列表l为空,就返回长度为0;否则,就计算l的尾部长度,并加上头结点的长度1,而计算l的尾部的函数,是通过(length length)来生成,其中length就是mk-length,故事就回到原点(mk-length mk-length)了,只是,其返回值在外围中要加1了,然后,在更外围中继续加1,加1,……。
但是,工作还没有完成,因为,mk-length中,((length length) (cdr l))很刺眼,它应该是(length (cdr l))这样的形式。重构,必须重构。必须在将其提炼成一个函数,因此,mk-length就变成
(define mk-length
  (lambda (length-mk)
    ((lambda (length)
    (lambda (l)
      (cond ((null? l) 0)
        (else (+ 1 (length (cdr l)))))))
     (lambda (x)
       ((length-mk length-mk) x)))))
代码似乎变得复杂些了,但效果是一样,并且,语法结构上基本保持一致。但是代码好像的确变得更长了,这也没办发,为了保持最内部length的纯洁性。但是,它也太深了,作为重点,应该放在外面,嗯,应该将两个lambda对调一下。
(define mk-length
  (lambda (length-mk)
    ((lambda (length)
       (length (lambda (x)
         ((length-mk length-mk) x))))
     (lambda (length)
       (lambda (l)
     (cond ((null? l) 0)
           (else (+ 1 (length (cdr l))))))))))
面对着这么多的lambda,实在难以淡定。但必须接收洗礼,方可体会到函数作为一等公民,所带来的强悍的表达能力,简直能撞破习惯命令式编程的眼球。里面的lambda(length)又变回原来的样子,但是,mk-length的主体已经不再是它了,而是一个以的lambda(length)为参数的lambda了。为了保持mk-length的纯洁,继续努力,这一次,是在两个(mk-length mk-length)上做文章,每次都要写两个相同的函数,不如把它做成函数。事情到了这一步,Y组合子已呼之欲出。
(define Y
  (lambda (f)
    (f f)))
((Y mk-length) '(a b c d e))    ;返回5
然后将mk-length中的第一条length的lambda搬过来,并且作为两个f的入参
(define Y
  (lambda (length)
    ((lambda (f)
       (f f))
     (lambda (length-mk)
       (length (lambda (x)
         ((length-mk length-mk) x)))))))
最后,将Y整得更加好看一点,也看来更加的通用,不仅仅是针对length,而是全部的需要递归的函数。
(define (Y f)
  ((lambda (g) (g g))
   (lambda (g)
     (f
      (lambda (x) ((g g) x))))))
再送上一道求和
((Y
  (lambda (sum)
    (lambda (n)
      (cond ((= n 1) 1)
        (else (+ n (sum (- n 1))))))))
 10)
文章已经很长了,打住。以后再发挥吧。

posted @ 2013-07-11 14:48 华夏之火 阅读(1786) | 评论 (2)编辑 收藏

2013年7月1日

C语言复杂声明的本质与局限

    先简单回顾一下C语言的独有的变量声明方式。自诩使用C语言多年,却一直对于C的复杂的变量声明方式头皮发麻,直到看到VCZH大神前不久的大作,才恍然大悟。惭愧,因此下面的内容颇有拾人牙慧之嫌,但为了引出后面一系列关于语言的随笔,也没办法了,本文的荣誉都归于vczh大神。就从最简单的说起。
    int a;    // 说明表达式a的值是int型,a自己本身也是int型,这不是废话吗?
    int array[N];    // 于是,表达式array[n]的值为int型,array是int数组,是否废话的味道少了一点?
    int *pA;    // 显然,*pA的值为int型,而pA的类型是指向int的指针。
    int fun(int x, int y)    // 毫无疑问,表达式fun(a,b)的值为int型,fun则是函数,其函数签名是……
    通过前面例子,说明一个道理,可以从另外一个角度来理解C变量的类型声明,先确定整个表达式的结果值的类型,再考察变量本身的类型。就好比以上几个例子,a(单独一个变量都是表达式), array[n], *pA, fun(a,b)这些表达式都是int型,定义变量的语句的类型,其实就是为了说明这个语句的变量的整个表达式的结果的值的类型。
    好了,请深呼吸,开始重口味了,下面的注释,其实都是废话。
    int *fuck[N];    // *func[n]的类型为int,因此,func[n]的结果类型为int*,因此,func的类型为数组,数组的元素为int的指针
    int (*pfuck)(int x, int y)    // (*pfuck)(a, b)的结果类型为int,看到(*pfuck),括号内出现一元操作符*,此物为求得指针的所指之内容,然后,此内容还能进行函数调用,因此可知,pfuck为指针,指向一函数,该函数的签名是……。当然,表达式pfuck(a, b)也可以得到相同的结果,但是,为了强调pfuck的类型,请坚持使用(*pfuck)。
    int* (*pfuck)(int x, int y)    // *(*pfuck)(a, b)的值为int,pfuck的类型自然是函数指针,函数签名是有两个int型的参数,其返回值是int*
    int (*func[5])(int *p);    // 毋庸置疑,(*func[i])(int *p)的结果是int型。它表示先获取数组的一个元素,对元素解引用,进而函数调用。显然,func为长度5的数组,数组元素是函数指针,函数有一int*行的变量,返回值是int型。
    int *(*func())();    // 心里发麻是不是,要淡定。不管怎么样,*(*func())()的结果始终都是int值,是不是?从最外围上看,*(...)(),此乃一函数调用,然后对返回值解引用得到的值为int。我们知道,C语言中,只有两物可进行函数调用的操作,或函数,或函数指针,两者必居其一。有以上例子分析可知,*(*func)()此乃对函数指针的函数调用结果求指针值。现在,又有*(*func())();,括号内的*func(),分明就表示func的函数调用,此函数的返回值为指针。结合最外层的函数调用,此返回值指针指向一函数,也就是说,返回值是函数指针。因此表达式*(*func())(),涉及到两个函数调用,它表示内层的函数调用返回函数指针,而此函数指针再调用一次,其结果为int*,再用上指针*运算符,整个表达式的值就为int了。因此,func是一函数,此函数返回函数指针,函数指针指向一个无参而返回值为int*的函数。曲折离奇,大功告成。

    好了,该反过来想了,如何从变量的类型来构造其定义语句。好比,“fuck指向一个数组,其个数为5,数组元素为函数指针,函数签名为带一个(int *p)参数,返回结果是int”。
    先考虑如何使用此变量。既然fuck是数组指针,那么,*fuck就是返回其所指向的数组,然后要得到数组的元素,自然理所当然必须用到[]操作符了,因此,就得到,(*fuck)[i]了,注意,千万切记,必须加括号,否则,*fuck[i]意味着fuck自己本身就是数组了。自己本身是数组,和指向数组,也即,数组和数组指针的差别,是相当大的,其差别之大就好像整型类型和整形指针类型。然后,必须不能忘记的是,一元操作符*就是取得指针的所指之物。
    好了,总之,对于fuck,我们通过(*fuck)[i]得到数组元素。既然元素又是函数指针,进而就得到,(*(*fuck)[i])(pa),这个表达式的值为int。因此,答案就是,“int (*(*fuck)[5])(int *p);”。
    代码写成这样子,真他妈的贱,尽玩文字游戏,写的人费心,读的人糊涂。这该死的C语言,shit!
    文章突然长了,打住。不惜对完整的类型进行分离,以求得声明与使用的语法的高度一致性。C语言真是,真是精致得让人大倒胃口。

    又:有时候,对于稍微复杂一点声明的常用类型,会经常出现重复的声明语法,特别是在函数指针的时候,为了拟补这种缺陷,或者说是痛苦,或者说是对于变量类型的重视,C语言提供了typedef的关键字。用以代表这种声明与使用的一致性的变量的类型。在前面的例子中看到,声明语句中的类型,只是说明变量采用这种表达式时,它的就是这种类型。好比,int *pArray[20],*pArray[i]的值为int型,但pArray却绝不是int型,为了取得pArray的类型,可借助typedef;具体的使用如下,typedef int* IntArray[20];,然后,IntArray pArray;以定义同样类型的变量。又好比上例,int *(*func())();这个函数声明好像让某些人难以理解,用上typedef化简一下,就可以重点很突出了:
    typedef int* (*FunFuck)();    // FunFuck代表无参返回值是int*的函数指针类型;
    FunFuck func();    // 作用相当于int *(*func())(),但含义更加鲜明。
    可以看到,typedef的用法很简单,不过是在过去的表达式的前面加一个typedef而已。后话,typedef在C++的template中,扮演了非常非常重要的角色,特别是模板元编程MPL中,全部的类型演算全部压在它身上,其作用之大,简直是惊天地泣鬼神,没有了typedef,C++的template不过是普通简单的泛型编程,有了template对typedef的完善支持,其实就是在struct/class内部中支持typedef语句,就导致了tmp的横空出现,导致C++的template成为威力最恐惧,同时语法也是最恐惧的泛型语言,没有之一。

    继续补充:加上const。以上对于复杂声明的理解,明眼人一看就知道仅仅是从右值的角度入手。要理解const,就必须不可不提到左值了。左值右值是C++中的基本概念,三言两语也说不清楚。最粗浅的看法,左值指可以被写入,也就是说能出现于赋值语句的左边;右值指可读,只能在赋值表达式的右边。当然,一般来说,左值往往可以当做右值来使用,可写往往意味着可读。而const的作用,就是将原本可写的东西,给整成只读的了。具体到表达式来说,就是某些表达式的值具备左右值,而const就是去掉了它的左值功能。举例说吧,还是从最简单说起。
    int a; 表达式a的结果是int型,既是左值又是右值;
    const int a;,a返回的结果是int类型,但是此结果已不再可写了,也即是a不能放在赋值语句的左边了。
    int const a; 可这样理解,先不理int const,a是一个变量,既可读又可写,const将a整成只读。int表示const a的结果类型。虽然,理解上这样,但对编译器来说,const int a;和int const a;都一样,都只是表达了同样的意思,a是一个整型常量。
    const int *p;,*p结果为int型,加上const后,*p只能读了,所以,p是整形指针,其所指的内容只能读不能写,但p本身却可写。 int const *p;,则先强调*p的只读性,然后再说明*p为int型。其实,这两种写法的意思都一样。
    int *const p;,const 紧挨着p,说明p本身只读。至于 int *,则表示*p类型为int,可读写。因此,p是整形指针,p本身不可写,但其所指的内容却可读又可写。说实在,不明白这样的指针变量有什么鬼用,实际的代码应该用的很少才是。
    为了表达指针的只读纯洁性的概念,不仅指针本身不能写,连其指向的内容也不可修改,C++终于整出了下面这样伟大的代码。int const *const p;或者const int * const p;。C++的这种做法,俗称致力于解决臆想中的问题,因为const与指针的组合,实际上只有指针所指向的内容只能读很有意义,其他的,意义微乎其微。
    可见,原本C的声明使用一致性的语法,遇上const之后,开始有点混乱了。当半路中杀出C++的引用之后,这种语法的一致性在C++中就不复存在了。C++的很多语言特性,都在不同程度不同角度,深度和广度上,形式和语义上,给C语法的精致性造成致命的各种各样的冲击。以至于,最后C++变成了有史以来很难很复杂超级变态恐怖的语言了,没有之一。

posted @ 2013-07-01 15:57 华夏之火 阅读(2070) | 评论 (6)编辑 收藏

2013年6月29日

键盘布局的改进之道

      好久没上博客了,自己的那么一点微末道行也不敢拿出来丢人现眼。实际上,过去的几年,真的是让C++和MFC害惨了,一直自个儿固步自封,说什么没有透彻掌握它们,绝不碰其他的玩意,结果就悲剧了,眼界相当重要,再怎么夸张都不为过。显然,MFC是垃圾,但实际上,C++也不是什么好菜,嗯,不吐槽了。还是做点更具实际意义的事情吧,今天的主角是键盘布局。
      由于历史的原因,当今流行的26字母的qwerty键盘布局并不是很科学,甚至有种说法,随便弄一个布局,都要比qwerty好,只因qwerty当初的设计意图就是为了最大限度的降低打字速度,这么说就有点过分了。不过,后来重新发明的布局,特别是DVORAK,的确比qwerty更具优势。但是,不管DVORAK的先天设计多么合理,如何在市场上如何造势,都不能撼动qwerty的主流地位。这很让人无语,由此可见,技术并不是决定市场的首要因素,关键是先占领市场,形成标准,不管这个标准有多差,只要有很多人遵守执行就行了,好比XX红色政党,又好比MFC,现在VC2012上居然还有他的一席之地,中国计算机图书还有那么多的VC书籍,实在令人疼心疾首。不过,本文的目的并非推荐DVORAK,当然,DVORAK键盘布局还是很值得广泛使用,但是既然已经如此的熟悉qwerty键盘了,那么也没有必要再训练了,实际上,qwerty已经足够日常使用了,我们平时打字,最大的限制在于大脑的速度,键盘布局到不是瓶颈。
      我要说的是,对于码农来说,键盘的另一不合理之处在于,小指的压力过大,左小指还好,只需负责Esc、~、……、A、Z等11个键位,先不论esc太远,这让vim情何以堪,而无关紧要的大小写切换键居然占据了那么优势明显好用之要地,等等无理设计。右小指表示压力更大,起码打了两倍,因为它的掌管比左小指的大了一倍之多,几乎是主键盘的1/4之多的键位,而且这些键,使用率都相当的频繁,如果再加上上下左右方向键还有delete,这实在太无天理了。可怜的两只小指,弱不禁风,娇怯怯,却要承受着生命难以承受之痛。相比之下,平时最能干的大拇指,居然只负责长长的空格键和两只alt这三个,这种不合理不公平的待遇,不禁让人怒从心头起,恶向胆边生,必须改革,彻底改革。给小指减负,给大拇指加负。
      当然,改革之前,先介绍windows下两大偷天换日的键盘修改利器,autohotkey和keytweak,其性能和使用说明,请各位自行百度谷歌。为了达到目的,老夫真的是挖空心思,无所不用其极。方法如下:
      1、借助autohotkey,将右手的所有键位都往右挪一格,也就是说,原来的7ujm这4个键,被发配到8ik,上,而8ik,就到了9ol.上,其他的以此类推,至于最右边的=\'/就只好屈居于开始时的7ujm上了。这样一来,小指起码少按了4个键位。右拇指只要愿意,可以不费力的按到右win键了,现在,两只拇指终于可以掌控四个键位了,四个很重要的键位,恩,目前除了space,其他三个似乎没啥特别,但很快,就会看到剩下来的三个中的其中一个,将发光发热,照耀整个键盘,最有作用。
      2、众所周知,上下左右home end 和翻页,这些键,其实也很重要,但是要按到它们,必须跑大老远,挥动右手做大幅度的机械运动。以至于,在vim和emacs中,都有各自的快捷方式来实现同样的功能。什么hjkl,什么ctrl+n,ctrl+p等等,不一而足,这种快捷方式,居然是其优于其他编辑器的亮点之一。但是,上下左右等键位可以配上ctrl、shift、win修饰键,然后马上就可以做出很多种组合,当然,emacs和vim也真是神通广大,针对每种组合,基本上都有对应的快捷键,只是记忆起来,实在麻烦。而区区在下,还曾经吭哧吭哧的拼命记忆过。好吧,好不容易习惯了vim和emacs那套逆天指法,却发现只能在vim或者emacs的环境下使用,屠龙之技,屠龙之技。我们要求的是,能够有一套放之于四海而皆准的指法,可以在所有的软件下,所有的场合下都发光发热。
      可能吗?确实有办法,就是在新键盘布局下,将右拇指能比之前轻松的按到的alt,摇身一变,变成换挡键,只要此键一按,马上wsad(游戏模式下的上下左右方向键)就变成上下左右了,配合jkl就可以组合出ctrl shift alt等效果。ec为home、end,rv则是上下翻页,f为esc键,各种各样,何其方便哉!剩下来的问题,就是右alt该何去该从,很简单,鹊巢鸠占,老实不客气,就占到右win键上,至于右win键怎么办,该怎么方便就怎么方便,甚至不存在,也没关系。制造换挡键,必须用到keytweak,autohotkey是不行的,鉴于数字小键盘上的除号实在很少用到,因此就拿他来当牺牲品了。其他的种种,请大家参考随文附上的ahk脚本。
      这样一来,只要稍加训练,键盘用起来将会很爽了。不爽的是,用别人的电脑,将特别的不习惯,各种难受。
      此外,右ctrl,可以用右掌腹来按,不必烦劳小指,他已经够累了。然后,汉字输入,要用拼音,最好是双拼,切记切记。不要在用什么五笔了,那是特别落后的输入法,其令人发指之处,可以和mfc值得一拼。在下曾经是五笔高手,下过苦功夫,多少个夏天夜晚,挥汗如雨,苦练五笔,一分钟达到百多字,绝对有资格说五笔的不是。现在我用双拼很高兴,已经不记得五笔的很多字根了,才不到半年的时间。
      其实,我都努力过,只是,都把汗水和精力,放在垃圾上了。C++是垃圾吗,当然不是了,但实际上,……,我觉得C++可以和粪便有得一比,作为肥料,还是很好地。

7::=
8::7
9::8
0::9
-::0
=::-

u::y
i::u
o::i
p::o
[::p
]::[
\::]

j::h
k::j
l::k
SC27::l
'::SC27

m::n
,::m
.::,
/::.

y::\
h::'
n::/

NumpadAdd::=
Shift & NumpadDel:: Send, {Backspace}

NumpadDiv & a:: Send, {Left}
NumpadDiv & d:: Send, {Right}
NumpadDiv & w:: Send, {Up}
NumpadDiv & s:: Send, {Down}

NumpadDiv & e:: Send, {Home}
NumpadDiv & c:: Send, {End}
NumpadDiv & r:: Send, {PGUP}
NumpadDiv & v:: Send, {PGDN}

NumpadDiv & z:: Send, {BackSpace}
NumpadDiv & x:: Send, {Delete}
NumpadDiv & f:: Send, {Escape}

CapsLock::LControl
LControl::Esc
Esc::CapsLock

NumpadDiv & k::
    Send {Control down}
KeyWait k  ; 等待用户释放按键.
    Send {Control up}
return

NumpadDiv & Shift::
    Send {Shift down}
KeyWait Shift  ; 等待用户释放按键.
    Send {Shift up}
return

NumpadDiv & Control::
    Send {Control down}
KeyWait Control  ; 等待用户释放按键.
    Send {Control up}
return

NumpadDiv & Alt::
    Send {Alt down}
KeyWait Alt  ; 等待用户释放按键.
    Send {Alt up}
return

NumpadDiv & #::
    Send {Win down}
KeyWait #  ; 等待用户释放按键.
    Send {Win up}
return

NumpadDiv & l::
    Send {Shift down}
KeyWait l  ; 等待用户释放按键.
    Send {Shift up}
return

NumpadDiv & SC27::
    Send {Alt down}
KeyWait SC27  ; 等待用户释放按键.
    Send {Alt up}
return

NumpadDiv & '::
    Send {LWin down}
KeyWait '  ; 等待用户释放按键.
    Send {LWin up}
return

posted @ 2013-06-29 02:56 华夏之火 阅读(1918) | 评论 (4)编辑 收藏

仅列出标题  下一页

导航

统计

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜