C++中的未定义和depends

先立个flag, 从此以后有三不聊:不聊政治,不聊宗教,不聊教育孩子。这三个主题有个共同的特点:大家都有自己的观点,大家都认为自己的观点对,但是其实谁的观点还都没啥用!而且这些话题插嘴的门槛还特别底, 这就是为啥容易吵吵起来的原因了。

所以我决定聊点C++语言,不好意思,这个热心网友想插嘴还不太容易。 今天主要聊聊我最近发现的C++的一个现象:那就是模板看起来很难, 其实不难; 异常看起来很简单,其实很难;多线程看起来很难,其实TMD更难。

首先聊模板,talk is cheap,show me the code。下面这段眼花缭乱的代码要是读懂还真不容易,不过他难就难在了语法上,其实就是利用编译期间,模板实例化的时候的SFINAE特点,来判定一个类是否有serialize这个函数。就这么简单。我知道你想骂人了,好端端的你就直接说中文就好,为啥总拽洋文呢? 因为这个词一旦翻译成中文,你要是看到了就想打人了。 这个单词就是“替换失败不是错误”这几个词的首字母缩写。不知道你怎么想,我个人觉得,同样是不懂,但是英文说出来更牛逼一点,是不?


template struct hasSerialize{
typedef char yes[1];
typedef yes no[2];
template struct reallyHas;
template static yes& test(reallyHas* /unused/) { }
template static no& test(…) { /* dark matter */ }
static const bool value = sizeof(test(0)) == sizeof(yes);
};

std::cout << hasSerialize::value << std::endl;

至于语法上花了呼哨的,其实就是一个习惯的问题,一旦你习惯了这种语法,你再看hello world啥的还觉得别扭呢! 好了总结一下,模板编程大部分都是语法上叠屋架床的,其实语义层面相当直接和简单。抓住了语义,那就是一拳到肉。至于语法开始麻烦点,大不了面向stack overflow编程就好。对了提醒一下,一般stack overflow上分为提问区和解答区,抄代码的时候一定要从解答区copy! 看看,这就是一拳到肉!

聊完了模板,现在聊聊异常,介绍C++异常的时候通常都会提到一个小函数,这个函数就是assert。这个函数简单的不得了,就是输入一个判断bool的表达式,如果表达式为false, 那么assert就终止整个程序。好吧,上代码:
assert(2+2==4);

就这么简单吗?对,就这么简单。那么下一个问题就来了,判断表达式判断啥?,assert该在哪里用呢?嗯~~, 这个depends。

我知道你又想打人了,刚刚明白了上面比较复杂的语法,现在好不容易遇到一个简单的,就是想知道在哪里用,你还说depends。这种感觉就像你把500块钱刚给了大街上一位向你招手的女孩,女孩收下钱后马上对你说你是一个好人。这就是一种想打人的感觉。 好吧,我换一种说法,assert就是用来验证是否违背了(invariant)不变性的时候用的。你现在感觉好点了吗? OK,那么什么又是(invariant)不变性呢? 举个例子吧,你今年20,再过一年你21,你遵守了(invariant)不变性,如果你今年20,过一年你还是20,那么你破坏了(invariant)不变性。来吧,打我啊!

没错,你没看错,(invariant)不变性其实是一个更大的概念,它就是指程序在语义上面是正确的,过了一年长一岁,这个在逻辑语义上是对的。 或者在一个更大的范围内是正确并且稳定的。例如,王健林为了让你完成一个小目标,给你转了一个亿。 在王健林的钱离开他的账号,但是还没到你的账号的时候,你们之间的(invariant)不变性就被打破了,直到钱转到你的账号上,你们之间的(invariant)不变性就又恢复了。不变性并不是指的“不变性”。验证这种(invariant)不变性其实是对编程以外的领域知识要非常了解的。例如:对一个人员管理系统,你应该用下面的assert语句。问题是,你真的确定100是正确的吗?你对全世界人口的极大值了解吗?
assert(age>0)
assert(age<100),

哪里会破坏(invariant)不变性呢,这个在多线程里面最多了。例如如果我们有两个线程分别计算你的岁数,每个线程把你的年龄加上0.5,这个时候如果同步的不对,就会发生data race。例如一个线程读入20,加上0.5后,还没写入到内存的时候,另外一个线程切入,读入20,加上0.5后,写回到内存。这个时候第一个线程再次回归,把自己计算得到的20.5再写一遍内存。最后两个线程结束后,内存里保存的就是20.5,而不是21了。这个时候你已经违背了(invariant)不变性了。

除了容易发生违背了(invariant)不变性以外,多线程另外一个问题就是语法和语义都很难,大量的模板推高了使用线程的语法的难度。语义上,哪怕一个简单的锁,什么时候用?哪里用?用不用?通通depends。 原子编程的内存模型又把语义方面的知识推到了CPU的instrument那个层次上去了。

上面说到多线程非常容易破坏数据的(invariant)不变性,首先这种破坏通常很难调试,因为他们并不是每次复现。另外破坏数据的(invariant)不变性还有一个更糟糕的结果,那就是程序的行为“未定义”。

C++新手看到“未定义”这一个词通常会有一种乐观的情绪。他们刚被“面向对象”糊弄到编程这个领域,既然是“未定义”,那么也许是“洞房花烛夜”也保不齐。一般C++老手会告诫新手,未定义通常是“洞房花烛夜-不举”。而10年以上的老鸟会说:未定义通常是“洞房花烛夜-新娘不举”。20年以上的骨灰这个时候一般是不说话的,他们一般45度斜头上望,眼睛里留下两行浊泪,嘴里嘟囔着:“那一年,我洞房花烛夜-新娘很举”。

希望你们能通过这个故事记住“未定义”这个词。对这个词到底有多坏一定要有充足的想象力。说简单点,标准委员会也不知道某些行为会坏到什么程度,那就叫做“未定义”吧。就像一个人长的丑得他妈都不愿意看,那么这种丑就叫做“未定义”。

总结一下:
1)模板在唬人的语法下面其实挺简单的,理解了它到底要干什么,那就一拳到肉了。
2)遇到depends的时候,通常意味着你需要更深入的领域知识了。
3)能不用多线程就不用,必须要用就用成熟的线程库和模型,例如boost线程池库,生产者-消费者模型,消息传递模型等。
4)如果你对“未定义”这个词感触不深,那么说明你还没到“那一年…”