刘未鹏|C++的罗浮宫

Knowledge sharing is the best reuse

原创 《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+_2功能是创建一个匿名的二元函数,它的作用是将两个参数相加然后返回相加的结果,相当于:

 

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也就有着落了。所以剩下的问题就是纯粹语法上的了,即“??”处用什么为占位符好呢?什么都不用不行,因为iter = cont.begin()C++里面是赋值语句,跟变量定义语句还是有区别的,C#里面早就加入了var关键字就是为这个目的,C++里面var估计早被用烂了。而auto刚好废物利用,auto也正好符合“自动”推导类型这么个意思,于是一个愿打一个愿挨,就这么凑活上了:-)

 

以上,就是C++09auto的故事。

 

延伸阅读

没有延伸阅读,这么简单的特性还要延伸阅读吗?:)

 

下期预告

本来这篇是要写lambda function的,因为最近scott meyersOn Software一个访谈里提到他认为C++09最有用的特性不是concept而是lambda。所以

等不及的可以先看这里这里,这两个提案。也可以到g9老大的blog上看这里这里java中的lambda functionjava里叫closure)的讨论,和对javascript里的。或者OCaml.cn上的这里。或者java closure的主要实现者的blog

 

--

我的讨论组

TopLanguage

发表于 @ 2007年09月07日 15:20:00|评论(loading...)

新一篇: 《C++0x漫谈》系列之:瘦身前后——兼谈语言进化 | 旧一篇: 玩的岁月

用户操作
[即时聊天] [发私信] [加为好友]
刘未鹏
订阅我的博客
XML聚合  FeedSky
订阅到鲜果
订阅到Google
订阅到抓虾
刘未鹏的公告
除非特别声明,本站采用Creative Commons License许可。转载请保留作者、出处。非商业。

重要公告

本博客已经迁移至 http://mindhacks.cn ,此处保留作为镜像,但不保证一定同步更新所有内容。原订阅 http://blog.csdn.net/pongba/rss.aspx (原始 Feed) 的朋友请转为订阅永久 feed : http://mindhacks.cn/feed/

关于

我经常在 @TopLanguage | @Twitter | @Douban

《C++的罗浮宫》5年选集

——知识分享是最大的复用

下载地址:csdn资源频道|mediafire

讨论问题请到TopLanguage综合技术讨论组

TopLanguage

精彩言论@TopLanguage


pongba的共享阅读@Delicious


pongba@Twitter


pongba在读@豆瓣


gtalk/msn(邮件请发送到gmail邮箱)

pongba@gmail.com
pp_liu@msn.com

搜索(不要回车,点击Go)


pongba翻译的





这个Blog上都写了哪些东东

文章分类
收藏
C++
Andrei Alexandrescu
Andrew Lumsdaine
Bjarne Stroustrup
boost
C++ Standard Commitee
Doug Gregor
Hans J. Boehm
Jaakko Jarvi
Jeremy G. Siek
Matthew Wilson
newsgroups
boost.Developer
boost.User
comp.lang.c++.moderated
comp.std.c++
TopLanguage
Open Source
Ant
codeplex
Danga
Google AJAX Search API
Google Code Prettify
Google Web Toolkit
Hadoop
MS shared source initiative
notepad++
STLSoft
不认识的朋友们
Delphij
fatalerror99
flow with the life
Glacier
jimaxsoft
lifesinger@淘宝UED
Mr. 6
realazy
Robbin
SpiritEpic
TK
wuyizi
Yelz
丁丁虫
付翀
冰云
刘慈欣
卢昌海
吴欣安(atppp)
周爱民
和菜头
姬十三
守望轩
小花@BlogBus
林达华
浦宇平
白鸦
程化
罗浩|Startup Game
阮一峰
霍炬
飞之鸿
高远
鲍盛
机器学习/数据挖掘/信息检索/自然语言处理/认知科学/人工智能
AAAI
Apex
arXiv
Charles Kemp
Christopher Bishop
Christopher Manning
Cognitive Daily
Dan Jurafsky
David MacKay
ECML PKDD
Geoffrey Hinton
Herbert Simon
ICML
IJCAI
Jeff Hawkins
Jiawei Han
JMLR
Josh Tenenbaum
Larry Wasserman
Lucene
Marvin Minsky
MIT AI Lab
MIT Computational Cognitive Science Group
Mitchell Marcus
ML
NetLab
NIPS
Peter Norvig
Stanford AI Lab
Stanford NLP Lab
Stephen Boyd
Tom Mitchell
Trends in Cognitive Science
Vladimir Vapnik
Weka
Zhihua Zhou
技术
Coding Horror
High Scalability
Reddit
Stack Overflow
Steve Yegge
代码发芽网
淘宝UED团队
淘宝数据仓库团队
玩聚网
移山之道
其它
Gigapedia
Scientific American
Scientific American Mind
科学松鼠会
科幻世界
认识的朋友们
alai
chenyufei
dd
DreamHead
Googol
Jawley
Joyfire
littlestone
lxwde
Matrix67
realfun
RiceBall
roofalison
soloist
Tinyfool
windstorm
YongSun
书剑
云风
余晟
元凯宁
冯大辉(Fenng)
刘新宇
刘江@图灵
史晓明
吴新雨
周星星
周筠@博文视点
孟岩
张志强|阅微堂
张振
徐宥|4G Spaces
方舟@博文视点
曾登高
李笑来|Pure Pleasure
杨军
杨文博
熊节
王信文
王康生
苏杰@阿里巴巴
范怀宇
荣耀
莫华枫
蒋涛
袁泳(g9)|负暄琐话
许式伟
谢东升
谷文栋|Beyond Search
邹欣@MSRA
郑昀
阿朱
陈冀康@华章
陈怀兴
鲍志云
存档
Csdn Blog version 3.1a
Copyright © 刘未鹏