《C++0x漫谈》系列之:Auto的故事
By 刘未鹏(pongba)
C++的罗浮宫(http://blog.csdn.net/pongba)
《C++0x漫谈》系列导言
这个系列其实早就想写了,断断续续关注C++0x也大约有两年余了,其间看着各个重要proposals一路review过来:rvalue-references,concepts,memory-model,variadic-templates,template-aliases,auto/decltype,GC,initializer-lists…
总的来说C++09跟C++98相比的变化是极其重大的。这个变化体现在三个方面,一个是形式上的变化,即在编码形式层面的支持,也就是对应我们所谓的编程范式(paradigm)。C++09不会引入新的编程范式,但在对泛型编程(GP)这个范式的支持上会得到质的提高:concepts,variadic-templates,auto/decltype,template-aliases,initializer-lists皆属于这类特性。另一个是内在的变化,即并非代码组织表达方面的,memory-model,GC属于这一类。最后一个是既有形式又有内在的,r-value references属于这类。
这个系列如果能够写下去,会陆续将C++09的新特性介绍出来。鉴于已经有许多牛人写了很多很好的tutor(这里,这里,还有C++标准主页上的一些introductive的proposals,如这里,此外C++社群中老当益壮的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重复劳动了:),我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。
Auto
上次说到,这次要说的是auto。此auto非彼auto,大家知道C++中原本是有一个auto关键字的,此关键字的作用是声明具有automatic(自动)存储期的局部变量,但跟register关键字一样,它也是个被打入了冷宫的关键字,因为C++里面的(非静态)局部变量本身就是auto的,无需多用一个auto来声明。
然而,阴差阳错的,auto在C++09中获得了新生。
问题
#1
还记得有多少次你对着这样的代码咬牙切齿的?
for(std::vector<int>::iterator iter = cont.begin(); iter != cont.end(); ++iter) {
// …
}
你根本不关心cont.begin()返回的具体类型是什么,你知道它肯定是一个迭代器。所以你其实想写的是:
for(?? iter = cont.begin(); iter != cont.end(); ++iter) {
// …
}
“??”处填入适当的东西。
况且,显式写出std::vector<int>::iterator还有一个坏处,就是当你将cont的类型从vector改为list的时候,这个类型也需要相应改变。简而言之,就是违背了所谓的DRY原则(或TAOUP中所谓的SPOT原则):同一份信息在代码中应该有一个单一的存放点。违反DRY原则被认为是很严重的问题,一份信息如果存放在两处地方,维护的负担就会增加一倍,修改一处便需要同时修改另一处;有人甚至提出代码中重复成分跟代码的糟糕程度是成正比关系的,不无道理。
有些书当中会建议你使用typedef来解决上面这个问题:
typedef std::vector<int>::iterator iter_t;
然后将使用std::vector<int>::iterator的地方全都改用iter_t。这样当你修改cont类型的时候,只需修改typedef一处即可。但typedef的坏处在于,你总归还是要写一个typedef,这个typedef的唯一作用便是为声明iter的地方提供类型,严格来说,这个typedef只是一个蹩脚的workaround。而且,此外这个typedef中仍然还是重复了std::vector<int>这一信息,为了去掉这一信息,又需要引入一个typedef:
typedef std::vector<int> cont_t;
typedef cont_t::iterator iter_t;
cont_t cont;
for(iter_t iter = cont.begin(); … ) {
// …
}
显然,这种做法很臃肿,并没有达到KISS标准。
另一方面,在许多脚本语言中,变量是没有类型的,我们只要写形如:
iter = cont.begin()
就行了。
很显然,在这个问题上,C++的类型系统给我们带来了麻烦。一门语言应该让我们可以不去关心根本不用关心的东西,将精力放在真正要做的事情上面,在这个例子中我们根本不关心cont.begin()返回的东西的具体类型是什么,我们只关心它能做什么(一个迭代器)。
#2
还有一次,我在使用boost.lexical_cast库,我写下:
std::string s = boost::lexical_cast<std::string>(i);
这里,std::string出现了两次,我明明已经告诉编译器我想把i转换为string了,却还要给s一个string类型——s的类型当然肯定是string了这还用说吗?除了白白磨损键盘之外,如果我后来要把i转换成其它类型的话,便要修改两处地方。
同一个项目中,我使用了boost.program_options:
unsigned long num_labels = vm["num-labels"].as<unsigned long>();
这跟上面的代码是同样的问题,unsingned long出现了两次。
#3
但所有这些都不是最严重的,因为毕竟你还知道返回类型是什么:你知道cont.begin()返回的是std::vector<int>::iterator,你知道lexical_cast<string>返回的是string,但是你知道:
_1 + _2
返回的是什么吗?
_1和_2是boost.lambda中的预定义变量,“_1+_2”的功能是创建一个匿名的二元函数,它的作用是将两个参数相加然后返回相加的结果,相当于:
unspecified lambda_f(unspecified _1, unspecified _2) { return _1 + _2; }
此处unspecified表示类型不确定,可以是int、long、等任何支持“+”的类型。boost.lambda通过一大堆元编程技巧来实现了这个功能。那么_1 + _2的类型到底是什么呢?
lambda_functor<
lambda_functor_base<
arithmetic_action<plus_action>,
tuple<
lambda_functor<placeholder<1>>,
lambda_functor<placeholder<2>>
>
>
> lambda_f = _1 + _2;
int i = 1, j = 2;
cout << lambda_f(i, j);
而且,这还只是boost.lambda最简单的表达式。
(不完美的)解决方案
对于#1,解决方案可以是std::for_each:
std::for_each(cont.begin(), cont.end(), op);
这就避免了每次声明std::vector<int>::iterator iter之苦,也不用显式iter++了。然而,缺乏语言内建的lambda表达式支持,std::for_each只能说是鸡肋。每次使用的时候都要跑到函数外面定义一个仿函数类(就算这个仿函数的逻辑只有一行,也要人模人样的写一个class定义出来),你说累不累啊?
在编码时,信息的局部性是很重要的,好的编码规范建议你在真正使用到一个变量的时候再去声明它,这样一个变量的声明点就紧紧靠在它的使用点上,一目了然(另外一个好处是有可能代码分支根本就执行不到这个变量声明点上,从而省去构造/析构该变量的开销),反之,另一种风格就是把所有(可能用到)的变量一股脑儿全都声明在函数的一开始,这个做法的问题是潜在开销以及可维护性负担。一个长达千行的函数,当我在后面看到某个变量,想看看它是什么类型的时候(变量的类型往往也能提供有用的信息),往上翻了老半天才找到(当然,有IDE的查找支持会好一些,但对象的构造析构开销依然存在)。
对于这里的仿函数op来说,对代码阅读者构成的影响是,读代码者必须转到op的类型的定义处(很可能要往上翻页才行)才能看到其逻辑是怎样的。此外,就算有IDE智能提示,op的问题还在于,如果它是state-ful的仿函数(即带有成员数据),就必须在构造函数里面把数据传进去,很麻烦。
(lambda function(也叫closure)的支持是另一个主题,我们下次讨论)
那么有没有更好的办法呢?不用写functor class如何?可以。
BOOST_FOREACH(int i, cont) {
// …
}
许多语言都内建了foreach,可见其重要性(本来循环就是编码活动中最常见的控制结构之一)。然而,foreach比之经典的for的能力从根本上却削弱了。foreach的循环是隐式的,每重循环我们只能看到这重特定循环访问到的那个数据i。而for循环是显式的,你不仅可以看到i,还可以看到迭代器当前所在的位置,之前之后的位置。比如说,在foreach里面,你不可能“记录下前一个位置”。
话说回来,foreach还是很有用的。尤其是当我们的逻辑是“对一个序列中的每个元素挨个做某件事情”的时候,使用foreach能够不多不少不肥不瘦的精确表达我们的意思,正所谓as simple as possible, but not simpler。
然而,这个方案毕竟只能解决for循环的问题,而且还要面临foreach的限制性。如果我仅仅只是要声明一个iter呢?
?? iter = cont.begin();
Andrew Koenig早在2002年的时候就在CUJ上发表了一篇文章——“Naming Unknown Types”,描述了对付这一问题的若干种方法:其中之一就是利用typeof,不过typeof毕竟不是语言支持的,只有部分编译器支持,而且typeof的问题在于,容易吸引人违反DRY