huaxiazhihuo

 

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 华夏之火 阅读(1390) | 评论 (2)编辑 收藏

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 华夏之火 阅读(2776) | 评论 (2)编辑 收藏

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 华夏之火 阅读(2560) | 评论 (6)编辑 收藏

键盘布局的改进之道

      好久没上博客了,自己的那么一点微末道行也不敢拿出来丢人现眼。实际上,过去的几年,真的是让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 华夏之火 阅读(2514) | 评论 (4)编辑 收藏

一点反省

    好久没有上博客了,再次看看之前的文章了,觉得很难受,除了批评MFC的那篇有点意义之外,其他都是在放屁,如果误人子弟了,在下很不安,并且里面还有很情绪化的倾向,本来应该删除,免得继续祸害初学者。但是,应该勇于面对自己曾经犯过的错误,就让它留着吧,只是祈求后来者,不要再看了。
    有一种错误的认识,说什么编程语言不重要,编程思想才重要,这种认识很没有意义,何为编程思想,只怕说这句话的人也不是很清楚,至于编程思想包含了哪些内容,那更加是她没法想象得到的广阔天地。当然,思想很重要,但是,无论多么高妙的思想,终究还得靠语言来表达。而且,有些语言表达某些思想,就是要比其他语言要直接,要直白,并且所谓的思想的有些理念本身就是语言的重要组成部分,这种语言还要求学习者要经历语言的洗礼,从而尽快的掌握所谓的编程思想,以便能初步使用它。然后,又有些语言天生残疾,无论如何整顿,就是没法表达某种编程思想,好比JAVA。总之,语言的选择,应该是很严肃的问题,不可等闲视之,至于思想,那也是通过具体的语言才能领悟。
    不管对于C++有多么深厚的感情,但是,确实没必要让自己吊死在一个树上。走出了那片天地,开阔视野之后,再回来,可以更好更快的处理原来领域上的问题。所以对于有志于CPPER,就是不要再C++各种语言细节上死钻牛角尖,必须多学习几种语言,吸收他们的优点,然后回来拟补C++本身的很多不足,学习其他语言,其实也是在学习C++,并且是更好地学习之法,因为这样,才能了解到C++的不足,才知道为了拟补其先天的不足,是怎样的努力,才设计出那些逆天的语言复杂性。
    个人感觉,C语言弱智(将太多的事情交给程序员来做,美其名曰信任程序员,搞得码农一天到晚死钻细节,又由于语言的抽象能力严重缺乏,所以很多事情就是吃力不讨好。当然也要承认,人写出来的代码,始终要比编译器出来的要好要可控,起码某种形式上,但是有必要吗)。至于C++,的确强大,自由,但是,她真的是丑陋无比,由于本身的野心过大,导致其语言核心无比的模糊,想要很好的使用,必须深刻系统的学习其静态类型系统和各种类型演算,因此,模板元编程必须基础十分扎实,不是为了用MPL写代码,而是学了之后,才能更深刻的感受到C++的类型系统(某些人十分反感template,不知所谓,殊不知,没有了template,C++的威力将降低一大半,还有,C++的各种奇技淫巧和宏,也在template这里大放惊人异彩。学了C#和java的泛型之后,才惊诧C++的template的功能如此的变态厉害,所谓的图灵完备,原来是这么回事)。此外,还要知道C++的种种不足,比如GC,比如严重残缺的动态类型信息,比如……(好吧,想不出来了),但是,这些都不是问题,C++非常神奇,对于语言上的种种不足,通过有些人种种奇技淫巧,历经千辛万苦,终于可以实现了,但是,最后出来的东西,始终还是没有语言层面上直接支持的来的好,并且有些还不免很丑陋,这是必须的,不过,C++11出来之后,语言的各种不足,都有不同程度的改善,造轮子时可用的材料也多了,最后,得益于C++11的新特性,轮子的外观也可以更好看。
    C++的学习教条,当然,重中之重,是,恳请不要学习C++,现实的很多很多问题,都可以不必C++来搞,那些嵌入式的东西,与硬件紧密相关的,C其实很能够胜任了,其实,C还是很好地。
    1、忌将精力都放在C++上,多学习学习其他语言;2、忌学MFC,这个东西,碰都不要碰;3、不要用C++解决所有的问题,很多问题用其他语言来解决效果会更好,无论开发效率还是运行效率;4、眼界很重要,……。作为C++的死忠,说这些实在有悖于对C++的感情,其实也不是,只是曲线救国之法,因为,要驾驭C++的种种奇怪的复杂,的确需要C++以外的视野。C++的确不是人人都能使用,合格的C++猿不但要拥抱奇技淫巧,还要能发明出更好的奇技淫巧,没有最好的奇技淫巧,只有更好的奇技淫巧,C++就是用来创造奇技淫巧的。
    窃以为,对于语言学习者来说,特别是C++猿,以下几种,C#的实用,scheme的优雅,haskell的严谨简洁,都有必要的学习学习。对了,还有smalltalk,但是考虑到面向对象本身的问题,就不推荐了。至于其他语言,在下接触不深,就不敢多嘴了。对了,还有设计模式,这东西还是很好的,特别是在面向对象的时候。至于以前说到的消息发送,纯属无稽之谈,这东西反人类,那是由于C本身的抽象能力不足,才搞出来的一个怪胎,无奈之举。对于比C更高级的语言,完全没必要再模仿了。设计模式的一大特点在于把模式的实现完全固守在静态类对象上,这样理解起来确实方便,但是,带来的问题,就误导了某些无知的读者,以为必须一定要用静态的面向对象的语言(在这一点上,JAVA是最彻底的实践者)来实现模式,以至于,为了实现模式,要写很多很多不必要的狗屎般的代码。其实,设计模式只是思想,书中的实现只是示范而已,大伙儿不必死盘硬套。对了,对于C++沉思录那道著名的例题,用设计模式做出来的效果,始终感觉很别扭,较好的思路是组合运算子。
    互联网上的有害信息真多,很不幸的是,之前写了那么几篇狗屁不通的文章,实在惭愧。

posted @ 2013-04-28 22:55 华夏之火 阅读(506) | 评论 (2)编辑 收藏

非理性拥护C++

        本来只想对C++赞叹复赞叹,后来就失控了,接着情绪化了,最后终于开始爆走,语无伦次。
        平心而论,C的而且确小巧精致,一切通通透透。老夫真心喜欢用它来编码,但一旦动用真格了,就立马叶公好龙,就会怀念C++的种种好处,class、 template、 virtual、 析构函数、甚至异常、const、引用等等,原来,离开了之后,才明白你的种种美妙动人之处,因此,朕已决定,有生之年,假如还在编码,那么C++,在心目中的,将是无可替代,它的一切,即便缺点,也是那么地令人回味无穷。因为它的一切,将自由贯彻到底,充分尊重用户的选择,不轻易剥夺用户的权利,更不强求用户用什么样的方式做设计。所谓自由的世界,独立的人格,手持C++利器,虽不敢说横行天下,但起码能愉快地编码。只有C++,当一个人独立使用,如此的耐人寻味,历久常新。多人一块开发,简直是大灾难,没必要的封装,种种自制的破烂轮子(前几年,出自本座手中的轮子不计其数,基本上惨不忍睹),错综复杂,交叉引用的类关系。这在其他语言中难以出现的怪现象,在C++中,平常得很,再一次证明了C++的博大精深,包罗万象。不说别的,就说C++中的最负盛名GUI框架MFC,其类层次的设计,糟糕透顶,而BCG的代码注入,毫无创意,笨拙无比的命名,垃圾般狗屎般的代码堆积,可怕的内存消耗,令人眼界大开,MFC的资源消耗已经够厉害,相比之下,居然显得那么节俭,而用BCG开发界面,居然比C#又或者JAVA做出来的软件,还不卡,这一切,都证明了C++过人之处。爱死你了,C++。
          近几年来看到某些人不知出于何因,对C++横加指责,说什么论效率不如C,论高级特性又不如其他的动态语言,实在莫明奇妙。说什么C++中的inline、继承、template破坏了模块的分离,“用C语言1000行源码能完成的工作千万不要用C++重写!”,实则用C++来写根本就无须1000行,并且可以精简那些字数多的代码行,并且还更加易读易懂,更加容易维护,效率或许还能更快一点点,得益于内联。如果还觉得用C++写1000行代码没有C那么漂亮,那只证明阁下没能力驾驭C++,请不要对C++乱加指责。他们那些所谓的C高手的代码,到处指针飞舞,又长又臭一再重复的表达式(本该内联大显身手),着实让人难受,当然,不否认他们的精妙设计。
        纵观他们对C++非议之例子,无一不暴露出其设计上的缺陷,本该成员函数指针大显伸手,他们却用上了虚函数;Template模式的函数(顺序依次,调用几本虚函数),本该做成全局函数,硬是整成员函数;多继承中的钻石抽象基类不该有任何东西,他们却偏要放某些东西,最后没办法,在虚继承中纠结。……所有这一切根本无损于C++,却只显现出他们的愚蠢与无知。想展现自己也言行独立,到头来却做出拾人牙蠢之事。其实,他们更应该感谢C++,是C++的包容,才容许了如此丑陋的设计。本座平生最不齿这群宵小,自己毫无主见,风闻名人几句惊世骇俗之话语,就跟着瞎起哄,国人的毫无道理的盲目跟风,由来已久,也不必细表了。那些所谓的C高手,觉得用C能做出精妙的设计,为何用起C++就不行了,其实他们大可“用C做设计,用C++编码”,这样,根本就不会影响他们的伟大杰作构思。
并且要做到如同C那样的高效,C++中完全没有问题,完全可以放下身段,将C++的抽象降低到C那样的级别,在没有独立完整的概念之前,或者是没有很好的理由,绝不用类来封装代码,禁用慎用C++的一切高级特性,好比虚函数、继承、异常等。任何语言特性都可以写出垃圾代码,也容易用得不好,但不可因为这样,就否定此种特性的价值。特性作用越大,就越微妙,就越容易滥用误用。即此而观,C++中,应该以class最为难用,此关一过,必定神清气爽。
的确,C中,你可以也必须面对一切细节,在这种恶劣的环境下,手上能用的武器,也只有函数、结构体、数组和宏,程序员的潜能就这样被迫出来,爆发出来了,做出最合乎本质的设计,而这几样简单武器,互相组合,居然可以用得如此出神入化,其效果鬼斧神工,巧夺天工,直可惊天地,泣鬼神,手法更是精彩缤纷,巧妙绝伦,令人目不接暇,但是,不管如何,始终缺乏管理细节的有效武器。
       鄙人最惊叹C++的一强悍之处,对于各种匪夷所思的变态问题,会有更加变态的解决方式,而且还不止一两种,更可见其灵活多变自由丰富的个性,但众多迥异特性又能如此和谐的共存,为什么?窃以为C++是强类型的静态语言,虽然提供多种语言工具以让码农愉快轻松地编码,尽可能地在编译时期发现更多错误,各种微妙的语言特性不过是为了帮助码农愉快高效地编码,少出错,他们可以用这些语言工具整理组织C的各种凌散的表达式。
因为C中虽然能直面一切细节,却缺乏管理细节的语言工具。所有C中的细节,几乎可通过C++的各种丰富特性妥善整理,而效率的损失又甚少,并且,在其强大的静态系统的分析,能多发现点问题。但是强类型只是工具而已,必须善加利用,但C++的码农不会受束缚,必要的时候,大可突破。鄙人就曾经实现了一个微型的动态系统,对象之间没有用层次关系,都是平等的,但之间又能互相组合装配拆除,达到多继承的效果,又没有多继承的各种问题。虽然语法上别扭点,但习惯了就感觉挺不错。
       要看到C++的对C代码的变态重组,为此,随便举例,qsort是代码上的典范境界,能排序所有的数组,只要提供了元素之间的比较函数,就能快速地排序,实至名归。但它是弱类型,其正确性全靠程序猿手工输入,参数出错了,编译器也检查不出来,当然C高猿不大容易出错。只是,依赖于C++强大类型推导威力,通过template整成以下样子,既不限制qsort的包容性,又不损失任何一点点效率
template<typename _Ty>
inline void Sort(_Ty* pItems, size_t nItemCount, int (__cdecl* funcCompare)(const _Ty&, const _Ty&))
{
    int (__cdecl * _PtFuncCompare)(const void *, const void *);
    union_cast(_PtFuncCompare, funcCompare);    // 为忽弄编译器的强类型检查
    qsort(pItems, nItemCount, sizeof(_Ty), _PtFuncCompare);
}
 但已经是强类型的了,C++猿用起来就不大容易出错了,并且元素的比较函数也更加容易编写,没必要再用指针了,个人而言,引用比指针好,最起码少敲一下键盘,那行代码的长度可减少了一个字符。这样,用起来不是更爽吗?
      又好比消息循环,判断消息类型,一遍又一遍地写着重复的表达式,好比,msg.message==WM_LBUTTONDOWN,不好玩,干脆class一CMsg,继承自MSG。好比这样:
class CMsg : public MSG
{
public:
    bool Is(DWORD nMsg) const{ return message==nMsg; }
};
         于是以上的那行判断语句,就精简成msg.Is(WM_LBUTTONDOWN),感觉应该好点吧。这两例的代码整理手段,对C++来说稀松平常,但C中就做不出来了,大概也只能用宏了,但宏的问题,大家也知道。
        又有人说,C++高手的修成要经过两次转换,从C到C++,然后从C++回复C,实在异想天开,不值一晒,舍弃C++的强大类型检查,欲与一切细节肉博,吾不见其高明。这不是什么C++高手,充其量也只是C高手,其苦心孤诣在C中模仿C++的面向对象的伎俩,用C++来表达,不过小菜一碟,并且还不失强类型检查,必要时,只须用联合体或类型转换忽悠编译器。那些回归C的高猿的C++代码,其实,不甚精致。所以,大家也不必理会。只须老老实实地做出简简单单的设计,然后再用C++组织管理各种细节,大可将代码写得漂漂亮亮干干净净。
         要谨记的是,只用那些熟悉有把握的语言特性,对于每一个用到的C++关键字,一定要清楚其背后的机制并且由此所带来的各种副作用。最难用的就是class了,毫无必要的封装, 比赤裸裸的代码更加丑陋,请优先选择非成员函数。封装的出现,是因为代码的一再重复出现的需要,而并非想当然地推理演绎。只要是重复代码,不管是一行表达,连续多行,分散跨行,都可以给予包装在一起,只需一个函数调用。
          再次重温C++的核心设计,尽可能利用静态强类型,尽可能地在编译期中找出程序的错误,提供多种丰富特性,协助码农充分地发挥强类型的一切优点,对抗一切细节,对抗一切重复代码,并且不必付出任何不必要的代价。当然,强类型只是忠实的奴仆,完全不必因为它而迁就你的设计,想要忽悠它,方法多种多样。 有人说,C++的语言特性太凌散,不系统,好像打补丁似的。但鄙人觉得挺好的,特性分散,各自为政,可随意自由组合,你讨厌某个特性,大可不必理睬,它就静静地站在一旁,丝毫不影响你的代码,这不就是设计的最高境界吗。
        好了,终于狠狠地出了口恶气。在下承认很情绪化,有失高手风范。

posted @ 2012-11-21 12:00 华夏之火 阅读(2614) | 评论 (16)编辑 收藏

略说成员函数指针及其模板的精简实现

    请容许先发一句牢骚,“这万恶的成员函数指针的丑陋语法!”,C中的函数指针的语法已经够难看的了,但相比之下,成员函数指针却更加不堪入目,使用上又很不方便,很不人性化,简直是只能行走寸步。只可惜,函数指针的作用实在太大了,忽视不得。
    大家都知道,函数指针(或又叫回调函数)是上层模块和底层模块进行通信的最佳手段,上层通过提供函数指针给底层,以使得底层在适当的时候,可以调用执行上层的代码,C库中的qsort足以说明这种使用,qsort在排序时,不知道如何比较两个数组元素的大小,必须通过上层提供的大小比较函数来进行比较。此外,操作系统提供的API中,有多少地方使用上了回调函数。假如没有函数指针,这简直没法想像,日子没法过了。函数指针是实现模块分层的不二法门,当然接口也可以,但是,用户代码必须继承或者实现底层硬性规定无理取闹的虚函数,本来很轻量级的POD,再也不轻快了,实在颇不方便,不,简直很有点恶心。说来说去,还是函数指针好。
    既然,C中的回调函数这么重要,那么,可想而知,进入C++中,整个世界到处都是CLASS,将回调函数这个概念推广到CLASS上,也即是成员函数指针,将是多么迫切的事情。理论上,可以将成员函数指针视为函数指针的语法糖,只要规定函数指针的第一个参数为void* pThis,然后在函数指针的实现函数中,进行类型转换也能满足使用,在很长的一段时间里,因为这种方式的简单清晰,一直都用这种方式代替成员函数指针。但是,一遍又一遍地被迫编写重复代码,特别是枯燥的类型转换,任何人都无法忍受。因此,决定直面这个问题。
    理论上,成员函数和普通函数一样,在内存中,都有自己的位置,只要有了地址信息,就可以通过指针来获取,保存起来,然后在未来的某个地方,通过这个指针来执行函数中的代码。差别之处,在于,调用成员函数前,要先将this推入ecx中,很久之前,成员函数指针确实和普通函数指针一样简单,只是后来,虚函数和多继承出现之后,简单的指针信息再也承载不了成员函数的重量,从此之后,.......(忽略,请大家自行BAIDU)。总之,C++中,成员函数指针并非指针类型,理所当然,也就不支持指针的大多数操作,比如,赋值NULL,或者类型转换。因此,所有能够让函数指针大放异彩的种种手段,在这里,都用不上了,原本在C中光芒四射的好东西,到了C++中,竟然黯然失色,所有本该让函数指针大显身手的地方,大家都绕道而行。
    逼急了,也有尝试突破,MFC的仅仅作了有限争取的手段(为了这一点点好处,MFC可不知作了多大的努力),居然成为其消息映射的基石。但是,据说,MFC的成员函数指针的设计也非出于自愿,而是因为消息太多,实在没法整成虚函数来处理,每个窗口类背负成千上万个函数的虚函数表,可不是省油的灯。为了努力地支持虚函数和多继承,C++的编译器不惜在成员函数指针的使用上设下种种阻拦,令人又气又恨。而更加令人不解的是,C++横行天下十几年,函数指针似乎长期得不到重视,大师们都在面向对象上探索,很多本该成员函数指针发光发热的地方,几乎都退位给虚函数了,并美其名曰策略模式又或者是其他的什么模式,不过是换了一套更加难看的马甲,却又那么好听的名字,不,不好听,只要听到模式两字,就令人大倒胃口。所有大用特用模式的代码,如果用非模式来实现,其代码量将少掉很多,而且还更具扩展性,这是真的。先透露一下,正在构思一文章,将深度介绍模式,专注于WHY,并且类比现实,兼扯上WINDOWS、COM和MFC对模式的应用,说句良心话,如果只用接口来做设计,模式绝对是好东西。只可惜,接口其实是SB。写底层代码,如果要求用户必须实现某些接口,又或者是继承某些类,改写虚函数,这种侵入式的设计,实在无理取闹之至。
    后来,大伙儿也终于开始重视成员函数指针,特别是C#的委托出现之后,网络上更是充斥着各种成员函数指针的版本代码,都可以很好地完成任务。特别是TR1又或者是BOOST中的function,功能相当的强悍得令人非大吃一惊不可。只可惜,大多数情况下,用户只想填饱肚子而已,但是BOOST或者其他的类库却硬要来一桌满汉全席,这也罢了,但是,它还要用户额外买单,并且还真不低呢,这就很让人受不了啦。其实,一般情况下,我们的要求不会太过分,仅仅想要针对普通的成员函数指针进行回调,它却为此在其内部new一个内部类出来,假如大规模使用,后果将不堪设想,其实也没那么严重,完成是C++迷们的强迫症。
    但是,说真的,实在希望很精简,不要生成不必要的虚函数表,不要模板生成不必要的函数(没办法内联,有函数地址的那一种),只要求它如同对待C中的函数指针一样,参数入栈和一个简单的函数调用的指令,外加将this推入ecx即可,就好像直接调用成员函数那样就好了。好了,贡献上代码了,史上最轻量级,精简无比的成员函数指针,功能也最弱了。对不起,代码并不完整,实际的代码,用上了宏,所谓的宏的图灵完备。在下很懒,一向只介绍想法而已,只于具体的实现细节以及语法考究,窃以为,每个C++迷们应该完全能够胜任。俗话说,高手只要求创意就行了,本文详细介绍算法并给出代码,已经落了下乘。
    这个实现,不考虑多继承,忽略了虚函数,也不支持非成员函数(也可以用上,只是,要多做一点点手脚,以下将给出示例,而且,普通函数指针已经完全可以胜任),只集中火力专注于普通成员函数,毕竟,在下的运用中,它占上了95%以上,所以才能如此的高效。单一职责啊!
    此外,本文参考了《成员函数指针与高性能的C++委托》、《刘未鹏的BOOST源码解析》、《C++设计新思维》,请自行GOOGLE。
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;
    typedef 
int ERROR_CantUseHorrible_cast[sizeof(InputClass)==sizeof(u) 
        
&& sizeof(InputClass)==sizeof(OutputClass) ? 1 : -1];
    u.
in = input;
    
out = u.out;
}

template
<typename FuncSignature>class TMemFn;
class CCallbackObject{};

template
<typename R>
class TMemFn<R ()>
{
public:
    typedef R ReturnType;
    ReturnType 
operator()() const{return (pThis->*func)();}

    template
<typename _Ty>
    
void Bind(_Ty* pObj, R(_Ty::*proc)())
    {
        union_cast(pThis, pObj);
        union_cast(func, proc);
    }

public:
    typedef ReturnType (CCallbackObject::
*FuncType)();
    FuncType func;
    CCallbackObject
* pThis;
};

template
<typename R, typename P1>
class TMemFn<R (P1)>
{
public:
    typedef R ReturnType;
    typedef P1 Param1Type;

    ReturnType 
operator()(Param1Type param1) const{return (pThis->*func)(param1);}

    template
<typename _Ty>
    
void Bind(_Ty* pObj, ReturnType(_Ty::*proc)(Param1Type))
    {
        union_cast(pThis, pObj);
        union_cast(func, proc);
    }

public:
    typedef ReturnType (CCallbackObject::
*FuncType)(Param1Type);
    FuncType func;
    CCallbackObject
* pThis;
};

template
<typename R, typename _Ty>
TMemFn
<R ()> MakeMF(_Ty* pThis, R(_Ty::*proc)())
{
    TMemFn
<R ()> res; res.Bind(pThis, proc);
    
return res;
}

template
<typename R, typename _Ty, typename P1>
TMemFn
<R (P1)> MakeMF(_Ty* pThis, R(_Ty::*proc)(P1))
{
    TMemFn
<R (P1)> res;res.Bind(pThis, proc);
    
return res;
}

int Test(int a)
{
    printf(
"Hello World %d\n", a);
    
return a;
}

int _tmain(int argc, _TCHAR* argv[])
{
    
class _CTest
    {
    
public:
        
int CallTest(int a)
        {
            
return Test(a);
        }
    };
    TMemFn
<int (int)> aTest = MakeMF((_CTest*)NULL, &_CTest::CallTest);
    aTest(
80);
    
return 0;
}

posted @ 2012-11-16 10:46 华夏之火 阅读(2923) | 评论 (4)编辑 收藏

轻量级共享对象的灵巧指针的实现

    毫无疑问,shared_ptr的功能不可谓不强大,设计不可谓不精巧,它的抽象级别不是一般的高,不仅要管理一般的C++内存资源,更染指其他的非C++资源,比如文件、比如连接、……,只要给它一个支点(释放资源的函数),不仅如此,还能顽强地生存于各种恶劣的环境,好比多线程、引用循环。当然,代价是有的,它背地里做了很多不为人知的勾当,表面上仅仅一行的带有构造函数shared_ptr的定义代码,编译器却要很无奈地生成一个莫明其妙的多态模板类(_Ref_count_base的继承类,带有虚函数表,意味着不能内联,用以在恰当的时机,释放资源),更别提要多了一堆指令,当然,在当今硬件性能蓬勃发展的美好时代,这点代价根本就不算什么,比之于那些什么所谓的虚拟机,甚至可以忽略不计。但是,总是有那么一批老古董,总会强迫假想自己写的程序会运行于各种资源非常苛刻的环境下,内心就是没法原谅shared_ptr所带来的极细微的损失。好比区区在下,每一次一用到shared_ptr,心里的那种负罪感啊,又多了几条废指令,又浪费多了十几个的堆字节,是否将生成内存碎片啊。终于有一刻顶不住了啦,去你妈的shared_ptr,老子不过想让你老老实实的代理内存资源,本本分分地做好你的分内之事,不劳你费心照顾其他的系统资源对象,那些场合本座自然有更好的解决方式。于是,制造轮子的悲剧又再次诞生了,虽然,他们一直在内心深处抵制新轮子的愚蠢行为,但是,……,只能说,知我者谓我心忧,不知我者谓我何求。
    每次想到shared_ptr要new一个_Ref_count_base的对象来管理计数,有人就恨得牙根发痒,巴不得把_Ref_count_base的数据成员搬过来,放之于那个要管理的对象的身上,以减少一小块内存。假如,客户传到shared_ptr构造函数的指针,此指针所指的内存,能再多几个字节(一个字节也行,最大值255,已足矣),以供我等存放一个long型的计数器,那就好办了。白痴也知道,这是不可能的事情。除非,此对象由shared_ptr来构造,那么还有办法再放多点额外内存进去。此话怎讲?大家都知道,C++中, new一个对象时,即意味着两步操作:1、分配一块内存;2、在此块内存上执行对象的构造函数。如果第1步的分配内存,能作多点手脚,比如说,分配一块比对象本身所占空间还要大的内存,那么我们的shared_ptr就可以把计数器放进对象之中了,也无须再new一个新的_Ref_count_base对象来管理计数器了。两块内存,合二为一,双剑合璧,妙哉妙哉。但,这如何做到呢?
    以下,是一个类从简单到复杂的物种进化历程。C++中,只要是通用类,即使再简单的需求,要写得可以被普遍承认,可以高高兴兴地到处使用,都绝非易事。而且,更悲剧的是,辛辛苦苦,呕心沥血造出来的轮子,还很有可能一问世就直接被枪毙,就算能苟且活下来,也不会有车愿意组装这一个废轮子。
    废话不说,书接上文,很明显,对象的new操作应该由我们的shared_ptr来掌控。任由用户来new,就太迟了,对象的内存块已经确定下来了,没文章可做啦。换句话说,shared_ptr必须模拟标准的new的两在操作分配内存和调用构造函数。由此可知,以下所探讨的shared_ptr运用场合也很有限,只适合于那些能看到构造函数并且知道其大小的C++类,所以,请大伙儿不要抱怨。唯其需求简单明确,所以才能高效。
首先,用一结构体__SharedObject来包含计数器和对象,如下所示:
struct __SharedObject
{
    
void* Object()    // 返回对象的地址,由于不知对象的类型,所以只能是void*,表示内存地址
    { return this+1; }
   
long Incref() { return InterlockedIncrement(&m_nRef); }
    
long Decref() { return InterlockedDecrement(&m_nRef); }
   
long m_nRef;
};
    是否很简陋,本座没法也不想把它整得更加好看了。
    我们的shared_ptr,就暂时叫TShared_Ptr好了,其包含的数据成员,暂时很简单。就只有一个__SharedObject的指针而已,后面由于多继承多态的原因,将被迫多增加一个指针。
    好了,先看看TShared_Ptr的使用之道,此乃class template。由于共享对象由TShared_Ptr所造,所以,在其诞生之前,首先势必定义一TShared_Ptr变量,好比,TShared_Ptr<int> pInt;考虑TShared_Ptr的构造函数,如果在里面就急急忙忙给共享对象分配内存,将没法表达空指针这个概念,所以它的无参构造函数只是简单地将m_pShared置为NULL。然后,TShared_Ptr必须提供分配内存并执行构造函数的操作,叫Construct吧;然后,析构函数也绝不含糊,执行对象的析构函数并释放内存。于是,TShared_Ptr的基本代码就出炉了。
template<typename _Ty>
class TShared_Ptr
{
public:
    TShared_Ptr() {m_pShared 
= NULL; }
    TShared_Ptr(
const TShared_Ptr& _Other)
    {    
        
if (m_pShared != NULL)
        {
            m_pShared 
= const_cast<__SharedObject*>(_Other.m_pShared);
            m_pShared
->Incref();
        }
        
else
            m_pShared 
= NULL;
    }

    
~TShared_Ptr()
    {
        
if (m_pShared != NULL)
        {
            
if (m_pShared->Decref() <= 0)
            {
                
if (m_pShared->m_nRef == 0)
                    DestroyPtr(
get());
                free(m_pShared);
            }
        }
    }
    _Ty
& operator*() const _THROW0() { return *get(); }
    _Ty 
*operator->() const _THROW0(){return (get());}

    
void Construct()
    {
        ::
new (m_pShared->Object()) _Ty();    // 调用构造函数
        m_pShared->Incref();    // 构造函数抛出异常,这一行将不执行
    }
    void alloc()    // 假设malloc总能成功
    {
        m_pShared 
= static_cast<__SharedObject*>(malloc(sizeof(_Ty)+sizeof(__SharedObject)));
        m_pShared
->m_nRef = 0;
    }

    _Ty 
*get() const _THROW0() {   return (_Ty*)(m_pShared->Object());}
    __SharedObject
* m_pShared;
};

可以写代码测试了,
    TShared_Ptr<int> pInt;
    pInt.Construct();
    (*pInt)++;
    TShared_Ptr<int> pInt1 = pInt;
    (*pInt1)++;
    咦,假如共享对象的构造函数带有参数,咋办呢?不要紧,重载多几个Construct就行了,全部都是template 成员函数。由于要实现参数的完美转发,又没有C++2011的move之助,我还在坚持C++98ISO,要写一大打呢,先示例几个,很痛苦,或者,可通过宏来让内心好受一点。
    template<typename T1>void Construct(const T1& t1);
    template<typename T1>void Construct(T1& t1);
    template<typename T1, typename T2>void Construct(const T1& t1, const T1& t2);
    template<typename T1, typename T2>void Construct(const T1& t1, T1& t2);
    template<typename T1, typename T2>void Construct(T1& t1, const T1& t2);
    template<typename T1, typename T2>void Construct(T1& t1, T1& t2);

    接下来就很清晰了,将shared_ptr的各种构造、赋值函数改写一遍就是了。然后,就可以高高兴兴地测试使用了。以上,是最理想环境下TShared_Ptr的很简单的实现,其操作接口多么的确简明扼要。
    开始,考虑各种变态环境,其实也不变态,完全很正当的需求。各种也不多,就两个而已:1、构造函数抛出异常,这个不是问题,由于TShared_Ptr的构造函数不抛出异常,其析构函数将被执行,检查到计数器为0,所以不调用共享对象的析构函数;
    2、多继承,这个有点头痛了。先看看代码,假设 class D : public B1, public B2,B1、B2都非空类;然后,B2* pB2 = pD,可以保证,(void*)pB2的值肯定不等于pD,也即是(void*)pB2 != (void*)pD。个中原因,在下就不多说了。但是,TShared_Ptr完全没法模拟这种特性,假如,坚持这样用,设pD为TShared_Ptr<D> ; 然后TShared_Ptr<B2>=pD,后果将不堪设想。一切皆因TShared_Ptr只有一条指向共享对象的指针,它还须拥有指向共享对象的基类子对象的指针,为此,必须添加此指向子对象的指针m_ptr,为_Ty*类型。因此,TShared_Ptr将内含两个指针,大小就比普通指针大了一倍,无论如何,到此为止,不能让它增大了。此外,TShared_Ptr增加了m_ptr成员后,还带来一些别的好处,类型安全倒也罢了,关键是在VC中单步调试下,可清晰地看到共享对象的各种状态,原来没有m_ptr的时候,就只能闷声发大财。
template<typename _Ty>
class TShared_Ptr
{
public:
    
    template
<typename _OtherTy>
    TShared_Ptr(
const TShared_Ptr<_OtherTy>& _Other)
    {    
        m_ptr 
= _Other.m_ptr;    // 类型安全全靠它了
        m_pShared = const_cast<__SharedObject*>(_Other.m_pShared);
        
if (m_ptr != NULL)
            m_pShared
->Incref();
    }
    
    __SharedObject
* m_pShared;
    _Ty
* m_ptr;
};
    本轮子自然不美观,使用也颇不方便,但胜在背地里做的勾当少,一切均在预料之中。好像多线程下,还有点问题,但那只是理论上的疑惑,实际运行中,该不会那么巧吧。
    咦,都什么年代,还在研究茴香豆的四种写法,在下也承认,确实没啥意义,但乐趣很无穷呢,我就是喜欢。珍惜生命,远离C++。对了,想要完整的代码吗,没那么容易

posted @ 2012-10-30 15:39 华夏之火 阅读(1703) | 评论 (5)编辑 收藏

难以割舍的二段构造

        两段构造也是声名狼藉得很,比之于MFC,好不了多少,貌似MFC中到处都是两段构造,难道两段构造的声誉也是受MFC所累。定义完了一个对象变量之后,还要再调用一次该对象的Create函数,而且还要Create成功了之后,才能对该对象做进一步的操作,否则对象将一直处于非法状态。这种代码方式写起来确实很恶心,为何不直接在构造函数中直接Create,不成功就抛出异常,然后对象就流产了,好过它半死不活地一直苟延残喘于世上,累己累人。其实,MFC选择两段构造也是有苦衷:1、先是很久很久以前,VC编译器对异常的支持不怎么好,当然,现在的VC编译器,自然今时不比往日,但是,还要兼容以往的代码;2、然后是MFC的设计,它只是对API做了一层薄薄的包装,薄薄的意思,就是,不管怎么捣鼓,都难以将WINDOWS系统中的各种对象包装成一个干净的C++对象了,因为,API本身就采用两段构造。可不是吗?定义一个句柄变量,然后CreateXXX返回结果,返回值非法,表示创建失败。失败了,还要霸王硬上弓,后果会怎么样,这谁也不知道。
        理论上,构造函数抛出异常确实很优雅,代码也更具美感,并且,其行为也更加明确,要么就处理,要么,就等着程序异常退出。但是,实际上,异常这种东西,真正实现执行起来,却相当的困难。更何况,如果完全丢弃两段法,除了异常,还会引入一些新的问题,正所谓:“前门驱虎,后门进狼”,进来不只是一只狼,而是好几只。生活的奥妙,就在于制造出新的问题,以解决旧的问题。
        构造函数中直接调用Create,就表示了用户一定义一个类型变量,程序就会马上启动Create函数,也就意味着可能将创建窗口对象、内核对象、甚至启动新的线程等等,这些操作都不是省油的灯,构造函数中做了太多事情,会有隐藏太多细节之嫌,代码本来就是为了隐藏细节,这个多事之罪名暂且不论;但是,用户没法对创建过程Say NOT,也即是说,用户一定义对象变量,就只能接受它的高昂的创建过程。难道,一开始就让对象进入有效状态,这都有错吗?确实是的。有时候,用户只是先想声明(定义)对象,等必要(时机成熟)的时候,再让它进入有效状态。咦,用户这样写代码,不太好吧,应该强制他/她等到了那个时候,再定义对象变量。变量怎么可以随随便便就定义呢?应该在要使用的时候,才定义它,这才是良好的代码风格。但是,有些情况,确实需要先暂时定义非法状态下的对象变量,比如,这个对象是另一个对象(拥有者)的成员变量时,那也没什么,强制用户在必要的时候,才定义拥有者对象变量。但是,假如这个拥有者必须是全局变量,那该怎么办?那也没什么,将拥有者定义为指针变量就是了?好了,本来只是要对象创建失败的情况,现在还要考虑内存分配的细节,然后接着就是new delete,然后就是各种智能指针闪亮登台演出,更糟糕的是,对象有效无效的问题依然没有根除,因为,只要引入指针,每次使用指针,就必须检查指针是否有效,咦,难道操作空指针不会抛出异常吗?C++规范中,操作空指针属后果未确定的行为,对C++而言,未确定往往就是最糟糕的意思。此外,鉴于对象只能一直处于有效状态,它就不可能提供让对象进入无效状态的操作。如果想要让对象无效,唯一的办法,就是让它死去,强制对象启动析构函数,方法是离开作用域强者delete它。下次要使用它的时候,就再new一次或者定义一次,不,它已经是另外一条新生命了。但是,对于两段构造的对象,只须Destroy又或者Create,对象可以永远只有一个。此外,二段构造颇具扩展性,很轻易地就可搞成三段构造,每一步,用户都有选择的权利。但构造异常就没有这个优点。
        考虑到构造函数中的参数问题,比如,月份的参数,大家都知道,有效值只在1-12月之间。不讨论这种情况下,非法的参数传递是否属于代码的逻辑问题。对此,构造异常指导下的对象是不可能出现无参(没有参数或者参数都有缺省值)的构造函数,因此,它们也都不能用于数组,难以应用于全局变量、静态变量、作为其他对象的数据成员,如果非要在这些场合下使用它们,比如占位符的作用,唯有用上指针,于是伴随而来的,又如上文所述,使用指针之前,必须检查指针的有效性,只怕不会比检查二段构造的有效性好多少。
        二段构造不轻易剥夺用户的权利,提供更多选择,可用于数组、堆栈、STL中的容器,要它死,它就死,要它活,它就活,但是,它可以从来都未曾消失过,要做的,仅仅是在使用它时,清楚它是死是活就行了,不过多加几次判断而已。相比之下,构造异常就更具侵入性了,一旦用上,就只能被迫遵照它的规则行事。
        其实,两段构造与构造异常,都很恶心,只要一处代码中用到了它,所有与之相关的代码都没法脱身。差别不过在于谁比谁恶心而已,这个,视各人的口味而不同。对于本人这种害怕分配内存,释放内存,更加畏惧异常的人来说(这并不表示本人写不出异常安全的代码),当然优先选择二段构造,MORE EFFECTIVE的条款中,声称,如无必要,不要提供缺省的构造函数,以免对象陷入半死不活的状态中。而我的习惯作法则是,如无必要,必须提供缺省的构造函数,不要轻易剥夺用户想要使用对象数组的权利,或者是由于不提供缺省的构造函数,而由此引起的种种不便。
        好了,既然程序中决定用二段构造了,那么,假如用户定义了一个对象,忘了再构造一次,但是又要执行其他操作,怎么办?嗯,那也没什么,既然用户不遵守契约,我们的对象自然可以做出种种不确定的行为。当然,别忘了,在其他的每一个操作上都添加几条assert语句,尽管这很恶心,也聊胜于无,减少点罪恶感,以便于在调试版中找出问题。

posted @ 2012-06-14 15:08 华夏之火 阅读(3719) | 评论 (14)编辑 收藏

C++沉思录课堂练习另解--消息发送(优化版)

     摘要:           缘起,看到一篇文章(懒得超链接),重新研究《C++沉思录》的那一个课堂练习,综合利用好几种设计模式,并且很好地遵守结合的面向对象的原则,嗯,不管怎么样,还是表扬这位同学的面向对象与设计模式的功力,确实不容易。只是,在下从模式堆里爬出来之后,已经对模式大倒胃口,看到这么小的练习,居然要用上这...  阅读全文

posted @ 2012-06-12 23:57 华夏之火 阅读(2073) | 评论 (0)编辑 收藏

仅列出标题
共5页: 1 2 3 4 5 

导航

统计

常用链接

留言簿(6)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜