从C++的一个特性到设计原则再到哲学

    最近在看C++的设计和演化,里面讲到算符重载。关于这个,Effactive C++里面明确说明,不要试图重载&&和||算符。因为这个重载造成的结果和默认不符(Not same with the default)。
    &&和||有什么特殊?熟悉C的朋友考虑这么一个问题。if(i && ++i)的作用是什么?基本来说,这个语句是判断i是否为0或者-1的,并且有个额外效果就是对i进行自增。但是,如果i == 0,则不进行自增,这就是&&的短路求值原则。这个原则产生了一系列写法,例如sh中常见的 [ -z "$ABC" ] && { … }。

    不过当重载了&&或者||后,就破坏了短路求值原则。因为C系列语言是应用序语言,参数先求值。所以后参数*一定*会被求值,无论前参数的值是多少。
    更加悲崔的是,这个破坏了最小惊讶原则,或者叫做知识内隐原则。当你使用一个知识的时候,你会根据自己的经验对这个知识做内隐的预期。例如,虽然螺丝有左螺纹也有右螺纹,然而你在拧螺丝的时候,多数预期是顺时针拧紧。不论其理由,这个已经成为常态。同样,有下压把手的门是扇页门,画着杯子的店家是咖啡店和茶馆,画着裙子的厕所是女厕,这些都是你对知识内隐的预期。破坏这个预期,相当于把螺丝改为反向,下压把手的门改成移门,画着杯子的店家是古董店,男厕画裙子一样,会让人感到不知所措。大家会莫名其妙的绕出去,确认门上画的确实是裙子,走进去再看到男厕,感到世界莫名其妙。

    同样的道理,如果一个对象使用了&&重载,程序员唯一能够快速发现的机会就是在调试时单步了&&的语句。如果他运气不好,可能在数个小时内都找不到理由,直到反汇编目标代码为止。
    那C++为什么设计算符重载?那是设计给需要的算符用的。其实C++一直是一个矛盾的设计,一方面他认为,程序员是不可信的,所以C++里面有隔离保护系统,例如私有成员函数和变量。另一方面,他又认为程序员应当对自己的行为负责,因此他设计了复杂的算符重载,复杂的继承系统,并期待程序员能够按照正确的方法使用。这是一个奇妙的,矛盾的设计思路,反映设计者自身的冲突(例如多人设计),或者C++设计者的实用主义倾向(选择最实用的设计)。python语言的思路相对统一,他认为程序员应当为自己的行为负责,所以python的隔离系统都是伪系统。而java的思路也相对统一,他认为程序员是不可信的,所以java才会搞出复杂的架构哲学。

为什么我说框架和工具不是解决安全性的良好方案

    在python-cn的maillist上,刚刚爆发了一场关于动态语言合并出错的争论。问题的起源,来自于这样一个问题。

一个程序员A,写了一个函数,function1。程序员B对函数进行了调用。现在两个人分别在svn上工作,A修改了function1,而b修改了其他内容。
由于python并不在编译时检查类别问题,因此当两人的svn merge后,运行并没有出错。现在,问题只有等上线后客户提出来了。
    几乎所有的人都同意,这问题的根源不是一个语言的问题。本质上说,这是一个工作流程问题。即使是C,也只检查参数的个数和类别,对于行为的变化和参数意义的变化还是无能为力的。
1.当你公开了一个函数,并要修改这个函数的外观行为的时候,必须向其他人通告。
2.python代码要通过unittest和黑盒检查覆盖。
3.代码应当cross review。
    争论的焦点主要是在python下如何避免这个问题。楼主Zhang Jiawei的观点是使用pydev,加上工具来检查。我,沈崴,ZQ的意见是通过行为来避免这个问题。所谓行为,主要包括以下几个。

1.互相review代码。
2.修改通告。
3.编写无检查和无处理的代码,并大量运行。如果代码中有错,程序会持续崩溃。因此当大量运行程序不崩溃时,代码就无错了。
    为什么我们并不推荐使用自动化工具来检测错误呢?主要是因为自动化工具可以*找到*问题,但是却不能*保证*是找到问题最彻底的一种。我举个最简单的例子:
网络工程师A,用了pylint,找到了自己code中的15个低级bug。他很高兴,因为工具使用起来很方便。
A向领导汇报了自己的心得,建议全公司推行这个工具。假定他的领导是项目经理B。
A:这个工具太好了,一下就找出了我15个bug,我发现用这个工具很方便,blahblahblah。
B:恩,很好,过两天你在公司里面讲讲这个工具。对了,你的code review做了么?
A:我用工具查过拉。
B:你确定他找出了你的*所有*bug么?
    问题的关键,就是*所有*。我们当然不可能找出程序中的所有bug。我所知的bug最少的程序是TeX,据说在数年的时间内只有数个bug。但是其版本号仍旧是3.1415926——正好是祖率的密率——而不是pi。我们毕竟不敢——高伯伯也不敢——保证没有bug。但是通过cross review,不处理加覆盖性检测,我们可以保证bug出现的概率在某个水平以下。
    自动化工具寻找出的bug,是在这个水准以上的。就是说,自动化工具看的出的,人应该看的出。人看的出的,自动化工具不一定看的出。如果做不到这点,说明你的水准还不足。
    所以,当我们需要一个尽量无错的code时,当你pydev/pylint,或者其他工具做了检测,问题是否解决了呢?没有,你仍旧需要review来保证没有bug。这样一来,工具的意义在哪里呢?
    当然,这并非说在做code review之前,你*不能*去做一遍代码扫描。只是说这样做并*不能替代*对错误的人工控制行为。
    除非你的目标是使用最低的成本,将错误减少到一个可接受的规模——而不是最低。就像我们在外包中常做的那样。这种情况下使用工具是比较合适的。
    而且一旦使用工具,很多程序员会产生依赖。所谓依赖,并不是讲从逻辑上他们不清楚在代码扫描外还需要独立的人工检测。但是在检测时,心里就会抱有一种放松的心态。尤其是其中某些虫族程序员让人无语叹息的行为。在中国的程序员界,有着诸多非常有创造力的bug提供者。例如擅长用str+=的java网页程序员很常见,这属于常见问题。但是自己写一套字典映射规则以完成数字到字符转换的(就是c下面的itoa).net程序员真的让我大开眼界——而且他同时犯下了str+=错误。要指望工具修正+=是可以的,要指望工具找出这类极品代码,估计下面会有更极品的人犯下更极品的错误。。。

专业程序员需要掌握的几种语言

受到这篇文章(http://blog.youxu.info/fyi/21-days/ )的启发,我突然想起对我所会的和要学的语言做个分类。确定一下专业的程序员到底需要会多少种语言。
1.系统类。只有C一个,必须学,而且需要在几个系统上编程就要学几次。学习系统类语言需要的是对系统结构和运行原理的了解,因此抽离系统的学习语法/抽象库/代码结构是没有任何意义的。
2.面对对象类。C#,Java等,推荐Java。构架方法优美大气,代码容易修改容易阅读,复用性好。然而做事上架梁叠屋,吃个馒头洗三遍手。可以学习构架方法,千万别学做事方法。
3.一门快速的脚本语言。Python, Php, Perl, Bash,各有特色。实际上如果你有空可以统统学一遍,非常有好处。快速脚本语言的特色就是整合其他代码和已经存在的东西,快速的构建出一个可用的程序。
4.一门语法抽象语言。目前只有Lisp和Scheme,推荐Scheme。这两种语言是在人工智能和符号推理的发展过程中产生的,因此对理解“机器是如何思考的”很有帮助。注意这两种语言的本质就是有限图灵机。
5.汇编。汇编语言种类太多,推荐80×86汇编。熟悉汇编语言对了解硬件和系统如何工作很有帮助,并且为查找系统内部(internal)的错误提供了便利。
按照上面的分类,程序员最少要会五种语言,我假定是C/Java/Python/Scheme/Asm80x86。C++不要学,那个是万恶之源。那么下面列举了我推荐的一些书单,可以由浅而深的学习这些语言。
1.入门,《21天学习C语言》《Dive Into Python》《80×86汇编基础教程》等等,这类书的目地是快速的教会是使用语言和语法。完成这个阶段的程序员可以找一些简单的题目做一下,但还不能独立完成普通程序的编写。
2.简单,《Think In Java》《数据结构与算法(Java语言版)》《设计模式》。这个层面基本涉及了数据结构,设计模式和编程方法。完成这个阶段后,可以找几个实际项目玩一玩了。
3.普通,《操作系统:设计和实现》《Unix系统编程》《windows核心编程》《TCP-IP详解》《Effective C》。这个层面涉及了系统运作原理和细节。完成这个阶段就可以写一些系统工具了。
4.阅读,《Python源码剖析》《深入浅出MFC》《Linux内核完全注释》。这个阶段注重阅读和积累各种代码经验。
5.专家,《计算机程序的构造和解释》《计算机程序设计艺术》《MIT算法导论》《数值算法》。通过前面的学习,普通程序编写应当已经不成问题。这个阶段面对的是将实际问题抽象成数学问题后,试图从数学上进行解决的过程。从此以上,就是数学的领域了。

关于Java和C++的一点争论

    不知道为什么,大家好像都喜欢争来争去。关于Java和C++的优劣不知道听了多少。碰巧我两者都会,怎么说也算是公平了吧。我就大着胆子,比较下两者的情况。
    对程序而言,速度不是最终要素。否则我们都应该去用汇编不是?一个程序有六个特性,易学,易用,安全,高效,可变,成本低。然而他们一般都是两两冲突的,好学了,就不好用。强大了,就不好学。安全了,自然要执行很多检查,高效了,自然不安全。针对某个平添优化了,可变性就很差,又不能移植,又不好修改。成本低了,自然什么都差。
    往往我们写企业应用的时候,都看重可变,低成本,安全,易用。高效呢?企业有钱买大服务器啊,这样的话效率差的不是太多也能接受。易学呢?企业有钱搞推广培训啊,只要有什么功能就加什么功能,不用考虑学不会。写用户应用的时候,则是看重安全,高效,然后易学易用里面要占据一样。可变呢?用户应用有多大?不行重写一个。低成本呢?这就是比较吊诡的事情了,没有啥经济效益的用户程序,往往是写起来最不怕费时最不管经济效益的。
    首先从性能角度来说。也许C++程序员说到这里就得意了,不过先别高兴。如果单论速度,汇编语言还在C之上呢。现在网络上很多人讲C优化好了比汇编快,Java优化好了比C快。听听都要笑掉大牙的。C再怎么快,完成同样的步骤,都需要这些汇编代码。Java再怎么快,完成一个动作,底下C代码也不会少的。所谓Java优化好了能快过C,不过是一个Java高手一门心思搞优化,加上碰到一个C语言白痴而已。
    我们先不讨论上面问题,就一般Java程序员和C++程序员而言(注意为啥我没说是C程序员,因为能自称纯C程序员的人要么非常精通语言,不会使用C++特性,要么就根本是个白痴),Java程序员编写出的代码效率比C++大约慢5倍上下。这个数据是我个人写两个程序,一个运算,一个读取处理,对比出来的。都是没有优化的代码。经过极端的优化后,C++的代码我大概提高了4-8倍的速度。可惜我不是个很好的Java程序员,Java代码的速度大概提升了一倍还不足。就是说,最终C++代码比Java快了将近15倍。
    但是C++程序员们先别乐,首先我Java语言并不好,这还不是最终的速度比。其次我牺牲了C++的很多特性。运算上几乎就是在写汇编了,接口都直接用了WIN32SDKAPI。没有移植性,没有可维护性,还需要特殊的技巧,怎么想都是牺牲重大。如果要真的这么追求速度,相信汇编会是更好的选择。我们在速度相差5倍左右的情况下就可以使用C++而不用汇编,为什么不能在速度相差5倍的情况下选择Java呢?
    然后我们再看性能的另外一个方面,存储管理。说简单点,就是外围设备吞吐管理和内存管理。这方面上C++也是远远超越了Java。不需要的内存就不要,不必须的吞吐就不吐。C++是门培养人的语言,没有很好的功底是无法驾驭的。C++是门程序员负责一切的语言,任何错误都是程序员的错误。然而对于Java来说,就不必处理复杂而没有意义的内存管理了。假设一个Java程序员需要传一个对象给子函数,他只要传递就可以了。然而如果C++程序员直接传递,那么就会出现参数拷贝过程。不但效率差,还可能出现错误。单单一个参数传递,就有三种方式。传值,传址,引用。又分成四类,静态动态,常量非常量。交叉起来,总共是12种情况,需要量材选用。如此烦琐的管理方式,我们可以想想对于内存来说是很有好处的。嵌入系统中大型程序设计绝对是C的天下。然而这么困难的使用方式,需要多大的人力成本才能做到呢?这明显的违反了低成本的原则。
    Java的内存管理从C++的角度看绝对是具有瑕疵的,内存释放了不管,直到没有空间了才收集。然而很多C++程序高手在特殊情况下,会重载operator new算子。其中的行为就很类似这个,Java只是将特殊情况下的应用放到了一般情况。这样对于速度的后果就是,很多的缓存会被持续的从物理内存中挤压出去,导致磁盘吞吐效率降低。对于Java程序,我猜测提升效率的瓶颈将会在和系统交互以取得最佳的垃圾收集时间上。
    下面的论题就可能是C++程序员所不高兴看到的了,安全性和可变性。
    就语言来说,用户输入的检查这种安全性是一点意义都没有的。我们所说的基本包括几个方面,非正常用法安全,线程安全,异常过程安全。就语言来说,分为了解释型和编译型。那么怎么区分呢?我这里有个很简单的方法,如果程序本身能被本身修改,就是编译型的,否则是解释型。注意,不是让你修改了磁盘上的源码再运行。这个能力赋予了C++强大的功能,例如修改游戏,检查病毒,都需要这个能力。但是不可否认的,比起无法修改自身的程序,安全性就差太多了。线程安全性上讲,Java的所有对象都是系统管理的,也就很容易的可以管理互斥。用过C++的都知道,C++自身是没有互斥的,全靠系统的函数库或者第三方库支撑。好用不好用不说,无法移植是一定的!最后是异常过程安全,C++的异常过程是很恐怖的。关键在于C++的异常传递有三种办法,传值,传址,引用。而且在异常过程中还又涉及回了内存管理啥的。天啊,要是异常都不能专心处理异常,我还要异常干嘛?
    至于可变性,那就更别说了。连处理异常都要小心内存泄露的家伙,你指望在修改代码的时候轻松到哪里去?C++是具备了强大的可变性,然而处于效率考虑,很多增强可变性的东西是选项的。例如RTTI,居然要开编译器选项的说。需要使用可变性就要牺牲性能,而且还要你小心的使用。如果使用不小心,抱歉,你又陷入效率和安全的问题里面去了。
    如果你有很好的系统功底,准备往计算机领域发展(注意不是计算领域)。那么C++是门很好的语言,只是非常费时而已。而且建议你学C#,VB或者Java作为第二语言,选择Bash或者Perl作为第三语言。这样在处理问题上可以事半功倍。如果你打算增加自己的计算机能力,方便日常的电脑使用,而不准备深入学习这个领域。那么只学C#,VB或者Java就足够了。
    最后要提到的是易学和易用,这两个特性都是软件设计所赋予的。也就是说,即使是以简单著称的VB.NET,也可以写很好用的程序。以复杂著称的汇编,也可以写很友好的界面。这就不在本文的论题以内了。

JNI试用记

    近两天要用JIN做数据加密系统,所以特别写了这篇。省得以后忘记,顺便造福大家。
    下面是核心编码:
//CTX.java
public abstract class CTX {
 protected byte[] state = new byte[20];
 protected long count;
 protected byte[] buffer = new byte[0]; /* input buffer */
 protected byte[] PADDING = { -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
   0, 0, 0, 0, 0, 0 };
 // 大端点序
 protected static void putChar(byte[] b, int off, char val) ;
 // 大端点序
 protected static void putLong(byte[] b, int off, long val) ;
 static {
  System.loadLibrary("cryptogram_jni");
 }
 public CTX();
 public void Update(String str);
 public void Update(char[] input) ;
 public void Update(byte[] input);
 public void Update(byte[] input, int length) ;
 public byte[] Final() ;
 public String GetHash() ;
 protected abstract void init();
 protected abstract void Transform();
}
//MD5_CTX.java
public class MD5_CTX extends CTX {
 protected native void init();
 protected native void Transform();
}
    然后导出h文件,在工程目录下执行这个。记得要先编译过代码,否则会说找不到。
javah -jni com.XXX.MD5_CTX
    如果要看Signature的话执行这个。同样需要先编译代码。
javap -s -p com.XXX.MD5_CTX
    把.h复制到c的工程目录,然后变换一下。
extern "C"
JNIEXPORT
void
JNICALL
Java_com_anhorn_cryptogram_MD5_1CTX_init(
 JNIEnv *env,
 jobject obj
){
 JavaCTX jctx(env, obj);
 jctx.jl_count=0;
 jctx.pd_state[0] = 0x67452301L;
 jctx.pd_state[1] = 0xefcdab89L;
 jctx.pd_state[2] = 0x98badcfeL;
 jctx.pd_state[3] = 0x10325476L;
 return ;
}
    如果按照原来的数据,马上会出现导出函数变成乱码的乌龙场景。其中JavaCTX的定义是这样的。
class JavaClass{
public:
 JavaClass(JNIEnv *env, jobject obj){
  this->env=env;
  this->obj=obj;
  this->cls=env->GetObjectClass(obj);
 }
protected:
 JNIEnv *env;
 jobject obj;
 jclass cls;
};
class JavaCTX : public JavaClass{
public:
 JavaCTX(JNIEnv *env, jobject obj);
 ~JavaCTX();
protected:
 jbyteArray jba_state;
 jbyteArray jba_buffer;
 jfieldID jfid_count;
public:
 jsize js_state_size;
 PDWORD pd_state;
 jlong jl_count;
 jsize js_buffer_size;
 PBYTE pb_buffer;
};
JavaCTX::JavaCTX(JNIEnv *env, jobject obj):JavaClass(env, obj){
 jfieldID jfid_state, jfid_buffer;
 cls=env->GetSuperclass(cls);
 jfid_state=env->GetFieldID(cls, "state", "[B");
 jba_state=(jbyteArray)env->GetObjectField(obj, jfid_state);
 js_state_size=env->GetArrayLength(jba_state);
 pd_state=(PDWORD)env->GetByteArrayElements(jba_state, JNI_FALSE);
 jfid_count=env->GetFieldID(cls, "count", "J");
 jl_count=env->GetLongField(obj, jfid_count);
 jfid_buffer=env->GetFieldID(cls, "buffer", "[B");
 jba_buffer=(jbyteArray)env->GetObjectField(obj, jfid_buffer);
 js_buffer_size=env->GetArrayLength(jba_buffer);
 pb_buffer=(PBYTE)env->GetByteArrayElements(jba_buffer, JNI_FALSE);
 return ;
}
JavaCTX::~JavaCTX(){
 env->ReleaseByteArrayElements(jba_buffer,
  (jbyte*)pb_buffer, JNI_COMMIT);
 env->SetLongField(obj, jfid_count, jl_count);
 env->ReleaseByteArrayElements(jba_state,
  (jbyte*)pd_state, JNI_COMMIT);
}
    这样就将Java中的一个特定对象在C++里面读了出来,然后下面大家都知道了。关键的native void Transform();在C++中的实现是Java_com_anhorn_cryptogram_MD5_1CTX_Transform。这个调用了MD5_CTX_Transform, which是我做的一个MD5算法。