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

2024-01-18 04:48
文章标签 c++ 系列 auto 故事 漫谈 0x

本文主要是介绍《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 ::iterator iter = cont.begin(); iter != cont.end(); ++iter) {

// …

}

你根本不关心cont.begin()返回的具体类型是什么,你知道它肯定是一个迭代器。所以你其实想写的是:

for(?? iter = cont.begin(); iter != cont.end(); ++iter) {

// …

}

??”处填入适当的东西。

况且,显式写出std::vector ::iterator 还有一个坏处,就是当你将cont的类型从vector改为list的时候,这个类型也需要相应改变。简而言之,就是违背了所谓的DRY原则(或TAOUP中所谓的SPOT原则):同一份信息在代码中应该有一个单一的存放点。违反DRY原则被认为是很严重的问题,一份信息如果存放在两处地方,维护的负担就会增加一倍,修改一处便需要同时修改另一处;有人甚至提出代码中重复成分跟代码的糟糕程度是成正比关系的,不无道理。

有些书当中会建议你使用typedef来解决上面这个问题:

typedef std::vector ::iterator iter_t;

然后将使用std::vector ::iterator 的地方全都改用iter_t。这样当你修改cont类型的时候,只需修改typedef一处即可。但typedef的坏处在于,你总归还是要写一个typedef,这个typedef的唯一作用便是为声明iter的地方提供类型,严格来说,这个typedef只是一个蹩脚的workaround。而且,此外这个typedef中仍然还是重复了std::vector 这一信息,为了去掉这一信息,又需要引入一个typedef

typedef std::vector 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 (i);

这里,std::string出现了两次,我明明已经告诉编译器我想把i转换为string了,却还要给s一个string类型——s的类型当然肯定是string了这还用说吗?除了白白磨损键盘之外,如果我后来要把i转换成其它类型的话,便要修改两处地方。

同一个项目中,我使用了boost.program_options

unsigned long num_labels = vm["num-labels"].as ();

这跟上面的代码是同样的问题,unsingned long出现了两次。

#3

但所有这些都不是最严重的,因为毕竟你还知道返回类型是什么:你知道cont.begin()返回的是std::vector ::iterator ,你知道lexical_cast 返回的是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 ,

    tuple<

      lambda_functor <1>>,

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

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

template

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 f = _1 + _2;

但这里也有一个问题,一旦赋给boost::function 之后,_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

void f(T t);

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

?? iter = cont.begin();

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

template

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




这篇关于《C++0x漫谈》系列之:Auto的故事的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/618113

相关文章

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

MySQL 设置AUTO_INCREMENT 无效的问题解决

《MySQL设置AUTO_INCREMENT无效的问题解决》本文主要介绍了MySQL设置AUTO_INCREMENT无效的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参... 目录快速设置mysql的auto_increment参数一、修改 AUTO_INCREMENT 的值。

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif

C/C++ chrono简单使用场景示例详解

《C/C++chrono简单使用场景示例详解》:本文主要介绍C/C++chrono简单使用场景示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友... 目录chrono使用场景举例1 输出格式化字符串chrono使用场景China编程举例1 输出格式化字符串示

C++/类与对象/默认成员函数@构造函数的用法

《C++/类与对象/默认成员函数@构造函数的用法》:本文主要介绍C++/类与对象/默认成员函数@构造函数的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录名词概念默认成员函数构造函数概念函数特征显示构造函数隐式构造函数总结名词概念默认构造函数:不用传参就可以

C++类和对象之默认成员函数的使用解读

《C++类和对象之默认成员函数的使用解读》:本文主要介绍C++类和对象之默认成员函数的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、默认成员函数有哪些二、各默认成员函数详解默认构造函数析构函数拷贝构造函数拷贝赋值运算符三、默认成员函数的注意事项总结一