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

转载 2007年09月18日 00:09:00

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

 

 

c++0x:auto 自动类型识别

c++0x中废除了原来c++中auto关键字的意义,赋予了它新的功能--自动类型识别。类似于c#中的var关键字。 例如: auto i=5;//int auto *p=&i;//int* a...

C++0x右值、move、forward、引用退化

昨天又学习了下右值、move和forward。记录一下学习到的东西: 1、引用退化 左值引用有传染性。左值引用的右值引用或右值引用的左值引用结果都是左值引用,即: string& &&和stri...
  • jjparch
  • jjparch
  • 2012年03月29日 19:44
  • 733

新C++标准:C++0x教程(一):导论

译者:yurunsun@gmail.com 新浪微博@孙雨润 新浪博客 CSDN博客日期:2012年11月12日 ...

对C#和C++0x中Lamda表达式的简略对比--lsp

Lambda表达式起源于函数式编程语言,后来逐渐被面向对象的编程语言所采纳。本文所讨论的不是Lamda表达式的使用方法,而是通过对比Lamda表达式在C#和C++0x中的不同实现而找出其中的区别。C#...
  • Augusdi
  • Augusdi
  • 2013年09月17日 15:08
  • 1741

VC10中的C++0x特性 Part 2 :右值引用

转自:VC10中的C++0x特性 Part 2 :右值引用 简介     这一系列文章介绍Microsoft Visual Studio 2010 中支持的C++ 0x特性,目前有三部分。 ...

C++0x概览 —— Bjarne Stroustrup

2005上海“Modern C++ Design & Programming”技术大会致辞 Bjarne Stroustrup C++0x 的工作已经进入了一个决定性的阶段。ISO C...

codeblock支持C++0x的设置

ubuntu: 1.Settings->complier and debugger->Global compiler settings找到other options  2.输入-std=c++...

c++0x 学习笔记之 lambda

转:http://feng.free.lc/?m=201104   有了 lambda 的支持之后,写一些函数式的代码更加方便了,比如 std::vector vec; std::for_ea...
  • zmlcool
  • zmlcool
  • 2011年08月27日 19:50
  • 348

C++0X Basic Knowledge

C++0x增加了诸多的feature,使C++俨然变成了新的语言,遂拙记一些。 Moving semantics C++ has supported copying object,but it...
  • huntzw
  • huntzw
  • 2012年09月03日 15:11
  • 463

Simpler Multithreading in C++0x

One major new feature in the C++0x standard is multi-threading support. Prior to C++0x, any multi-th...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:《C++0x漫谈》系列之:Auto的故事
举报原因:
原因补充:

(最多只允许输入30个字)