分类: C/C++
2013-11-22 17:04:49
在思考怎么写这一篇文章的时候,我又想到了以前讨论正交概念的事情。如果一个系统被设计成正交的,他的功能扩展起来也可以很容易的保持质量这是没错的,但是对于每一个单独给他扩展功能的个体来说,这个系统一点都不好用。所以我觉得现在的语言被设计成这样也是有那么点道理的。就算是设计Java的那谁,他也不是**,那为什么Java会被设计成这样?我觉得这跟他刚开始想让金字塔的底层程序员也可以顺利使用Java是有关系的。
难道好用的语言就活该不好扩展码?实际上不是这样的,fgsj1122但是这仍然是上面那个正交概念的问题。一个容易扩展的语言要让你觉得好用,首先你要投入时间来学习他。如果你想简单的借鉴那些不好扩展的语言的经验(如Java)来在短时间内学会如何使用一个容易扩展的语言(如C++/C#)——你的出发点就已经投机了。所以这里有一个前提值得我再强调一次——首先你需要投入时间去学习他。
正如我一直在群里说的:"C++需要不断的练习——vczh"。要如何练习才能让自己借助语言做出一个可扩展的架构呢?先决条件就是,当你在练习的时候,你必须是在练习如何实现一个从功能上就要求你必须保证他的可扩展性的系统,举个例子,GUI库就是其中的一类。我至今认为,学会实现一个GUI库,比通过练习别的什么东西来提高自己的能力来讲,简直就算一个捷径了。
那么什么是扩展呢?简单的来讲,扩展就是在不修改原有代码的情况下,仅仅通过添加新的代码,就可以让原有的功能适应更多的情况。一般来讲,扩展的主要目的并不是要增加新的功能,而是要只增加新代码的前提下修改原有的功能。譬如说原来你的系统只支持SQLServer,结果有一天你遇到了一个喜欢Oracle的新客户,你要把东西卖给他,那就得支持Oracle了吧。但是我们知道,SQLServer和Oracle在各种协议(asp.net、odbc什么的)上面是有偏好的,用DB不喜欢的协议来连接他的时候bug特别多,这就造成了你又可能没办法使用单一的协议来正确的使用各种数据库,因此扩展的这个担子就落在你的身上了。当然这种系统并不是人人都要写,我也可以换一个例子,假如你在设计一个GPU集群上的程序,那么这个集群的基础架构得支持NVidia和AMD的显卡,还得支持DirectCompute、Cuda和OpenCL。然而我们知道,OpenCL在不同的平台上,有互不兼容的不同的bug,导致你实际上并不可能仅仅通过一份不变的代码,就充分发挥OpenCL在每一个平台上的最佳状态……现实世界的需求真是orz(OpenCL在windows上用AMD卡定义一个struct都很容易导致崩溃什么的,我觉得这根本不能用)……
在语言里面谈扩展,始终都离不开两个方面:编译期和运行期。这些东西都是用看起来很像pattern matching的方法组织起来的。如果在语言的类型系统的帮助下,我们可以轻松做出这样子的架构,那这个语言就算有可扩展的类型了。
编译期对类型的扩展
这个其实已经被人在C++和各种静态类型的函数式语言里面做烂了。简单的来讲,C++处理这种问题的方法就是提供偏特化。可惜C++的偏特化只让做在class上面,结果因为大家对class的误解很深,顺便连偏特化这种比 OO简单一万倍的东西也误解了。偏特化不允许用在函数上,因为函数已经有了重载,但是C++的各种标准在使用函数来扩展类型的时候,实际上还是当他是偏特化那么用的。我举个例子。
C++11多了一个foreach循环,写成for(auto x : xs) { … }。STL的类型都支持这种新的for循环。C++11的for循环是为了STL的容器设计的吗?显然不是。你也可以给你自己写的容器加上for循环。方法有两种,分别是:1、给你的类型T加上T::begin和T::end两个成员函数;2、给你的类型T实现begin(T)和end(T)两个全局函数。我还没有去详细考证,但是我认为缺省的begin(T)和end(T)全局函数就是去调用T::begin和T::end的,因此for循环只需要认 begin和end两个全局函数就可以了。
那自己的类型怎么办呢?当然也要去重载begin和end了。现在全局函数没有重载,因此写出来大概是:
template auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }
template my_iterator begin(const my_container& t);
template my_range_iterator begin(pair range);
如果C++的函数支持偏特化的话,那么上面这段代码就会被改成这样,而且for循环也就不去找各种各样的begin函数了,而只认定那一个std::begin就可以了:
template auto begin(const T& t)->decltype(t.begin()) { return t.begin(); }
template my_iterator begin< my_container>(const my_container& t);
template my_range_iterator begin< pair>( const pair& range);
为什么要偏特化呢?因为这至少保证你写出来的begin函数跟for函数想要的begin函数的begin函数的签名是相容的(譬如说不能有两个参数之类的)。事实上C++11的for循环刚开始是要求大家通过偏特化一个叫做std::range的类型来支持的,这个range类型里面有两个static函数,分别叫begin和end。后来之所以改成这样,我猜大概是因为 C++的每一个函数重载也可以是模板函数,因此就不需要引入一个新的类型了,就让大家去重载好了。而且for做出来的时候,C++标准里面还没有 concept,因此也没办法表达"对于所有可以循环的类型T,我们都有std::range必须满足这个叫做 range_loopable的concept"这样的前置条件。