《C++0x漫谈》系列之:Auto的故事

C++0x漫谈》系列之:Auto的故事

By 刘未鹏(pongba)

C++的罗浮宫(http://blog.csdn.net/pongba)

C++0x漫谈》系列导言

这个系列其实早就想写了,断断续续关注C++0x也大约有两年余了,其间看着各个重要proposals一路review过来:rvalue-referencesconceptsmemory-modelvariadic-templatestemplate-aliasesauto/decltypeGCinitializer-lists…

总的来说C++09C++98相比的变化是极其重大的。这个变化体现在三个方面,一个是形式上的变化,即在编码形式层面的支持,也就是对应我们所谓的编程范式(paradigm)C++09不会引入新的编程范式,但在对泛型编程(GP)这个范式的支持上会得到质的提高:conceptsvariadic-templatesauto/decltypetemplate-aliasesinitializer-lists皆属于这类特性。另一个是内在的变化,即并非代码组织表达方面的,memory-modelGC属于这一类。最后一个是既有形式又有内在的,r-value references属于这类。

这个系列如果能够写下去,会陆续将C++09的新特性介绍出来。鉴于已经有许多牛人写了很多很好的tutor这里这里,还有C++标准主页上的一些introductiveproposals,如这里,此外C++社群中老当益壮的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重复劳动了:),我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。

Auto

上次说到,这次要说的是auto。此auto非彼auto,大家知道C++中原本是有一个auto关键字的,此关键字的作用是声明具有automatic(自动)存储期的局部变量,但跟register关键字一样,它也是个被打入了冷宫的关键字,因为C++里面的(非静态)局部变量本身就是auto的,无需多用一个auto来声明。

然而,阴差阳错的,autoC++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_2boost.lambda中的预定义变量,“_1+_<chmetcnv w:st="on" tcsc="0" numbertype="1" negative="False" hasspace="False" sourcevalue="2" unitname="”">2<span lang="EN-US" style="FONT-FAMILY: 宋体; mso-ascii-font-family: Verdana; mso-hansi-font-family: Verdana"><span lang="EN-US">”</span></span></chmetcnv>功能是创建一个匿名的二元函数,它的作用是将两个参数相加然后返回相加的结果,相当于:

unspecified lambda_f(unspecified _1, unspecified _2) { return _1 + _2; }

此处unspecified表示类型不确定,可以是intlong、等任何支持“+”的类型。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,比如上面这个,如果写成:

typeof(cont.begin()) iter = cont.begin();

很明显罗嗦得一塌糊涂。还不如std::vector<int>::iterator呢。而且typeof也只能推导出一个表达式的类型,并不能提取任何我们想要的类型,比如我们想要一个函数f的第二个参数的类型,就不能用typeof。这些原因也是C++98不肯支持typeof的原因(不过时隔十年,typeof终究还是要进入C++,因为泛型编程的需要早就超出了当年语言设计者的预期,这是后话,等到讲decltype的时候再提)。

那么怎么办呢?Koenig提供了另一个办法——辅助函数。因为在C++中,函数模板具有自动推导出参数类型的功能,所以:

template<typename T>

void aux(T iter);

aux(cont.begin());

这个方案很显然太差了,Koenig也只是拿来当反面教材而已。aux的参数iter的作用域根本就超不出aux的定义,所以与声明一个局部变量iter有本质的差异。

type-erase

type-erase是一项看上去很fancy而且也的确实用的技术。对于像C++这样的静态语言来说,type-erase带来了实质性的差异。拿上面#3来说,_1+_2的类型非常复杂,乃至于手动声明它根本是不可行的,那怎么办呢?除了立即把_1 + _2传给一个函数模板,如:

std::transform(cont1.begin(), cont1.end(), cont2.begin(), cont3.begin(), _1 + _2);

之外,就没有其它办法能够将它“暂存”到一个变量中吗?有的。type-erase使之成为可能:

boost::function<int(int, int)> f = _1 + _2;

但这里也有一个问题,一旦赋给boost::function<int(int, int)>之后,_1 + _2便“坍缩”为一个只能将两个int相加的仿函数了。不管你在boost::function<...>的尖括号内填什么,_1 + _2都会不可避免的坍缩。

(对boost::function如何实现这一点有兴趣的话,可以参考我以前写的boost源码剖析之:boost::function,也可以参考C++ Template Metaprogramming里面的type-erase一节(但注意,内有元编程慎入))

显然,这个方案也并非完美。

害羞的类型推导系统

Haskell里面,一个被广为赞誉的特性就是type inference。本来type inference是一个挺简单的东西,任何静态语言,从某种程度上,都必须跟踪表达式的类型。然而由于haskell把这一点在语言层面暴露得实在太好,所以type inference竟成了一个buzz wordC++自有模板开始就支持type inference,模板参数推导正是其体现。然而可惜的是,C++的类型推导系统非常害羞,明明可以推导出一切表达式的类型,却偏偏犹抱琵琶半遮面,为什么这么说呢?

大家都知道sizeof能够获取任何表达式的结果的大小:

sizeof(/*arbitrarily complex expression*/)

而要知道一个对象的大小,就必须先要知道其类型。因此,C++的语言引擎是完全能够推导出任何表达式的结果类型的。可以说,sizeof背后隐藏了一整个类型推导系统。MCD里面也正是通过这个sizeof实现了一系列的技巧,从此打开了潘朵拉的魔盒。boost.typeof更是无所不用其极,居然通过sizeof和一系列的元编程技巧实现了一个模拟的typeof操作符。

话说回来,虽然C++明明能够推导任何表达式的类型,然而语言层面却硬是不肯开放typeof接口,搞得元编程的老大们费尽了心思,吐出五十两血来才搞定一个还不能算完美的typeof

早该如此——auto涅磐

既然

template<typename T>

void f(T t);

能够推导出它的参数类型,而不管其实参是多么复杂的表达式。那么要语言级别支持:

?? iter = cont.begin();

其实根本不用费任何劲。只要合成出一个函数模板:

template<typename T>

void f(T iter);

然后利用现成的模板参数推导,便可以推导出iter的类型了,一旦有了iter的类型,声明iter也就有着落了。所以剩下的问题就是纯粹语法上的了,即“??”处用什么为占位符好呢

你可能感兴趣的:(编程,C++,c,C#,haskell)