社区应用 最新帖子 精华区 社区服务 会员列表 统计排行 银行

  • 26790阅读
  • 45回复

C++0x2011年8月10日获通过正式成为国际标准

级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
— 本帖被 usidc5 执行置顶操作(2011-08-14) —


C++0x最终国际投票已于周三结束,所有国家都投出了赞成票,C++0x已经毫无疑义地成为正式国际标准。虽然接下来还需要数月时间出版发布,但标准本身已经完全正式成立了。
C++0x是目前计划中的C++编程语言的新标准。它将取代现行的C++标准ISO/IEC 14882,公开于1998年并于2003年更新,通称C++98以及C++03。新的标准将会包含核心语言的新机能,而且会扩展C++标准程序,并入了大部分的C++ Technical Report 1程序库(数学的特殊函数可能除外)。因为此项标准尚未完成,记载于此条目的可能并不是C++0x最新的情况。最新的消息被公开在ISO C++ 委员会网站(英文)。国际标准草案(N3126)于2010年8月公开,2010年11月对该草案进行了修订


===============================================

日前,C++ Connections Conference for C++09 Preparation在位于阿拉伯联合酋长国首都迪拜的芝加哥海滩宾馆顺利闭幕。

  此次会议讨论通过了一些全新的将加入C++09标准的特性,并且决定将一些本来预备在2009年才正式加入标准的内容提前作为C++标准的一部分提供给广大C++程序员。包括之前众所周知的核心改进Rvalue References和Type Concepts,以及初始化时候的类型自动推演、Delegating Constructors、NULL Pointer等特性都已经加入当前C++标准。STL原有相当部分都经过修改甚至重写以达到更优的稳定性和效率,并且在保留原有接口的前提下增加了相当数量的新接口,对此会议主持者之一Thomas Tung表示,虽然这增加了C++的学习难度,但是由此提供的极为丰富的新功能实在是值得掌握,况且函数的很多默认参数也给C++的初级用户提供了便利。而TR1中几乎所有成熟的内容都被提前加入了STL中,包括完善的正则表达式库、智能指针、封装好的数组和多维数组、高精度运算、跨语言支持等等内容。一些数据结构也新做了相当实用的封装,包括线段树、用于不相交集合的并查集、斐波那契堆、检索树、双端优先队列等等。图部分,丰富的算法使之成为一大亮点,包括拓扑排序问题以及各种生成树问题、最短路问题和网络流问题等都提供了高效灵活的解决方案。原本作为map等实现方案的红黑树也在增加接口后直接提供给程序员使用。而另外一大批准备加入的泛型算法则将等到2009年再发布,这将可能使标准库中的泛型算法个数激增到500以上。

  在新标准出台后,g++方面表示会在近期内放出符合标准的编译器。而其他编译器厂商都表示近期不会做出大的更新。微软表示会在VS2009发布时对这些新内容进行支持。

  对于如此巨大的更新,C++之父Bjarne Stroustrup却表示这只是C++09的冰山一角。一些更为关键的改进将在09年发布。其中包括提供更高效率和更灵活应用的Strength Inheritance技术,使工程开发更轻松的Workgroup Encapsulation机制,帮助减少错误的Early-Warning Debugging方法等等。这些内容加入后,C++将再次脱胎换骨,甚至成为一门全新的语言。

  “我们将迎来的是一场全新的革命,跨度就像从航空到航天那么大。”Bjarne Stroustrup自豪地说。而由于改变过大甚至导致大量C++程序员需要重新培训,Bjarne说:“相信我,这是值得的。”

  面对优势开始变得过于明显的C++,ACM/ICPC(ACM国际大学生程序设计竞赛)的负责人Dany Terra表示将考虑增加比赛难度或者对使用C++的选手进行技术限制以保证公平和对选手水平的充分考验。但是其他比赛的举办方,如Topcoder,都表示对C++的日益强大表示欢迎。

  Primer C++、Thinking in C++、Effective系列等著名C++书籍的作者都表示已经开始为09年新标准的发布进行准备,会争取在第一时间出版新版著作。
关键词: C++ 编程 技术
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 沙发  发表于: 2011-06-23
C++11语言标准主要变化

C++11语言标准即将正式发布,C++标准委员会前成员Danny Kalev解释了新标准的主要变化和改进
C++作者Bjarne Stroustrup最近说,C++11就像一门新语言。确实,C++的核心有重大变化。现在它支持Lambda 表达式,对象自动类型推导,委托构造函数(delegate constructor),控制默认函数,nullptr,原生字符串标识,右值引用,等等。
中文版C++0x FAQ


译者前言:
很快很快,最新的ISO C++标准C++0x就要来到我们身边了。
今天获得Stroustrup先生的许可,开始翻译由他撰写和维护的C++0x FAQ。我觉得这是一件伟大而光荣的事情,但是我又觉得压力很大,因为我的英语水平很差劲,同时自己的C++水平也很有限,很害怕在翻译过程中出现什么错误,贻笑大方不要紧,而误人子弟就罪过大了。所以,我这里的翻译只能算是抛砖引玉,如果你的英文很好,你可以直接阅读他的原文。或者,你也可以参照两者进行阅读,我想一定会有更多的收获。
当然,我也非常欢迎大家指出翻译中的错误,或者是加入进来和我一起翻译这份文档,共同为C++0x在中国的推广做一点事情。你可以通过chenlq at live.com联系到我。
对自己的翻译做一点说明:
    在翻译的过程中,尽量遵照原文含义,可能有时候也会自己根据自己的理解加一点批注,希望可以帮助大家理解。另外,虽然C++0x标准还没有正式公布,但是现在已经有很多编译器支持C++0x,比如gcc以及它在Windows下的MinGW,Visual C++ 2010也部分支持,大家可以使用这三款编译器尝试这个文档中的例子。在下面的目录中,已经翻译的问题链接到相应的中文文档,未翻译的问题则链接到英文原文。
感谢所有参与翻译的志愿者:interma,Chilli,张潇,dabaidu,Yibo Zhu,lianggang jiang,nivo,陈良乔
最后,祝大家阅读愉快:)
---------------------------------------------------------------------------
目录
关于C++0x的一般性的问题:



关于独立的语言特性的问题:

我经常从提案中借用一些例子。所以,我要感谢这些提案的作者们。另外,我也从自己的访谈和论文中借用了很多例子。

关于标准库的问题:

QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 板凳  发表于: 2011-06-24
C++11中值得关注的几大变化
赖勇浩(http://laiyonghao.com


声明:本文源自 Danny Kalev 在 2011 年 6 月 21 日发表的《The Biggest Changes in C++11(and Why You Should Care)》一文,几乎所有内容都搬了过来,但不是全文照译,有困惑之处,请参详原文(http://www.softwarequalityconnection.com/2011/06/the-biggest-changes-in-c11-and-why-you-should-care/ )。
注:作者 Danny Kalev 曾是 C++ 标准委员会成员。
Lambda 表达式
Lambda 表达式的形式是这样的:
view plaincopy to clipboardprint?
[capture](parameters)->return-type {body}  


来看个计数某个字符序列中有几个大写字母的例子:
view plaincopy to clipboardprint?
int main()  
{  
   char s[]="Hello World!";  
   int Uppercase = 0; //modified by the lambda  
   for_each(s, s+sizeof(s), [&Uppercase] (char c) {  
    if (isupper(c))  
     Uppercase++;  
    });  
cout<< Uppercase<<" uppercase letters in: "<< s<<endl;  
}  


其中 [&Uppercase] 中的 & 的意义是 lambda 函数体要获取一个 Uppercase 引用,以便能够改变它的值,如果没有 &,那就 Uppercase 将以传值的形式传递过去。


自动类型推导和 decltype
在 C++03 中,声明对象的同时必须指明其类型,其实大多数情况下,声明对象的同时也会包括一个初始值,C++11 在这种情况下就能够让你声明对象时不再指定类型了:
view plaincopy to clipboardprint?
auto x=0; //0 是 int 类型,所以 x 也是 int 类型  
auto c='a'; //char  
auto d=0.5; //double  
auto national_debt=14400000000000LL;//long long  


这个特性在对象的类型很大很长的时候很有用,如:
view plaincopy to clipboardprint?
void func(const vector<int> &vi)  
{  
  vector<int>::const_iterator ci=vi.begin();  
}  


那个迭代器可以声明为:
view plaincopy to clipboardprint?
auto ci=vi.begin();  


C++11 也提供了从对象或表达式中“俘获”类型的机制,新的操作符 decltype 可以从一个表达式中“俘获”其结果的类型并“返回”:
view plaincopy to clipboardprint?
const vector<int> vi;  
typedef decltype (vi.begin()) CIT;  
CIT another_const_iterator;  
统一的初始化语法
C++ 最少有 4 种不同的初始化形式,如括号内初始化,见:
view plaincopy to clipboardprint?
std::string s("hello");  
int m=int(); //default initialization  


还有等号形式的:
view plaincopy to clipboardprint?
std::string s="hello";  
int x=5;  


对于 POD 集合,又可以用大括号:
view plaincopy to clipboardprint?
int arr[4]={0,1,2,3};  
struct tm today={0};  


最后还有构造函数的成员初始化:
view plaincopy to clipboardprint?
struct S {  
int x;  
S(): x(0) {} };  


这么多初始化形式,不仅菜鸟会搞得很头大,高手也吃不消。更惨的是 C++03 中居然不能初始化 POD 数组的类成员,也不能在使用 new[] 的时候初始 POD 数组,操蛋啊!C++11 就用大括号一统天下了:
view plaincopy to clipboardprint?
class C  
{  
int a;  
int b;  
public:  
C(int i, int j);  
};  
C c {0,0}; //C++11 only. 相当于 C c(0,0);  
int* a = new int[3] { 1, 2, 0 }; /C++11 only  
class X {  
  int a[4];  
public:  
  X() : a{1,2,3,4} {} //C++11, 初始化数组成员  
};  


还有一大好事就是对于容器来说,终于可以摆脱 push_back() 调用了,C++11中可以直观地初始化容器了:
view plaincopy to clipboardprint?
// C++11 container initializer  
vector vs<string>={ "first", "second", "third"};  
map singers =  
  { {"Lady Gaga", "+1 (212) 555-7890"},  
    {"Beyonce Knowles", "+1 (212) 555-0987"}};  


而类中的数据成员初始化也得到了支持:
view plaincopy to clipboardprint?
class C  
{  
int a=7; //C++11 only  
public:  
C();  
};  
deleted 函数和 defaulted 函数
像以下形式的函数:
view plaincopy to clipboardprint?
struct A  
{  
A()=default; //C++11  
virtual ~A()=default; //C++11  
};  


叫做 defaulted 函数,=default; 指示编译器生成该函数的默认实现。这有两个好处:一是让程序员轻松了,少敲键盘,二是有更好的性能。
与 defaulted 函数相对的就是 deleted 函数:
view plaincopy to clipboardprint?
int func()=delete;  


这货有一大用途就是实现 noncopyabe 防止对象拷贝,要想禁止拷贝,用 =deleted 声明一下两个关键的成员函数就可以了:
view plaincopy to clipboardprint?
struct NoCopy  
{  
    NoCopy & operator =( const NoCopy & ) = delete;  
    NoCopy ( const NoCopy & ) = delete;  
};  
NoCopy a;  
NoCopy b(a); //编译错误,拷贝构造函数是 deleted 函数  
nullptr
nullptr 是一个新的 C++ 关键字,它是空指针常量,它是用来替代高风险的 NULL 宏和 0 字面量的。nullptr 是强类型的:
view plaincopy to clipboardprint?
void f(int); //#1  
void f(char *);//#2  
//C++03  
f(0); //调用的是哪个 f?  
//C++11  
f(nullptr) //毫无疑问,调用的是 #2  


所有跟指针有关的地方都可以用 nullptr,包括函数指针和成员指针:
view plaincopy to clipboardprint?
const char *pc=str.c_str(); //data pointers  
if (pc!=nullptr)  
  cout<<pc<<endl;  
int (A::*pmf)()=nullptr; //指向成员函数的指针  
void (*pmf)()=nullptr; //指向函数的指针  
委托构造函数
C++11 中构造函数可以调用同一个类的另一个构造函数:
view plaincopy to clipboardprint?
class M //C++11 delegating constructors  
{  
int x, y;  
char *p;  
public:  
M(int v) : x(v), y(0),  p(new char [MAX])  {} //#1 target  
M(): M(0) {cout<<"delegating ctor"<<end;} //#2 delegating  


#2 就是所谓的委托构造函数,调用了真正的构造函数 #1。
右值引用
在 C++03 中的引用类型是只绑定左值的,C++11 引用一个新的引用类型叫右值引用类型,它是绑定到右值的,如临时对象或字面量。
增加右值引用的主要原因是为了实现 move 语义。与传统的拷贝不同,move 的意思是目标对象“窃取”原对象的资源,并将源置于“空”状态。当拷贝一个对象时,其实代价昂贵且无必要,move 操作就可以替代它。如在 string 交换的时候,使用 move 意义就有巨大的性能提升,如原方案是这样的:
view plaincopy to clipboardprint?
void naiveswap(string &a, string & b)  
{  
string temp = a;  
a=b;  
b=temp;  
}  


这种方案很傻很天真,很慢,因为需要申请内存,然后拷贝字符,而 move 就只需要交换两个数据成员,无须申请、释放内存和拷贝字符数组:
view plaincopy to clipboardprint?
void moveswapstr(string& empty, string & filled)  
{  
//pseudo code, but you get the idea  
size_t sz=empty.size();  
const char *p= empty.data();  
//move filled's resources to empty  
empty.setsize(filled.size());  
empty.setdata(filled.data());  
//filled becomes empty  
filled.setsize(sz);  
filled.setdata(p);  
}  


要实现支持 move 的类,需要声明 move 构造函数和 move 赋值操作符,如下:
view plaincopy to clipboardprint?
class Movable  
{  
Movable (Movable&&); //move constructor  
Movable&& operator=(Movable&&); //move assignment operator  
};  


C++11 的标准库广泛使用 move 语义,很多算法和容器都已经使用 move 语义优化过了。


C++11 的标准库
除 TR1 包含的新容器(unordered_set, unordered_map, unordered_multiset, 和unordered_multimap),还有一些新的库,如正则表达式,tuple,函数对象封装器等。下面介绍一些 C++11 的标准库新特性:


线程库
从程序员的角度来看,C++11 最重要的特性就是并发了。C++11 提供了 thread 类,也提供了 promise 和 future 用以并发环境中的同步,用 async() 函数模板执行并发任务,和 thread_local 存储声明为特定线程独占的数据,这里(http://www.devx.com/SpecialReports/Article/38883)有一个简单的 C++11 线程库教程(英文)。
新的智能指针类
C++98 定义的唯一的智能指针类 auto_ptr 已经被弃用,C++11 引入了新的智能针对类 shared_ptr 和 unique_ptr。它们都是标准库的其它组件兼容,可以安全地把智能指针存入标准容器,也可以安全地用标准算法“倒腾”它们。


新的算法
主要是 all_of()、any_of() 和 none_of(),下面是例子:
view plaincopy to clipboardprint?
#include <algorithm>  
//C++11 code  
//are all of the elements positive?  
all_of(first, first+n, ispositive()); //false  
//is there at least one positive element?  
any_of(first, first+n, ispositive());//true  
// are none of the elements positive?  
none_of(first, first+n, ispositive()); //false  


还有一个新的 copy_n:
view plaincopy to clipboardprint?
#include <algorithm>  
int source[5]={0,12,34,50,80};  
int target[5];  
//从 source 拷贝 5 个元素到 target  
copy_n(source,5,target);  


iota() 算法可以用来创建递增序列,它先把初值赋值给 *first,然后用前置 ++ 操作符增长初值并赋值到给下一个迭代器指向的元素,如下:
view plaincopy to clipboardprint?
#include <numeric>  
int a[5]={0};  
char c[3]={0};  
iota(a, a+5, 10); //changes a to {10,11,12,13,14}  
iota(c, c+3, 'a'); //{'a','b','c'}  


是的,C++11 仍然缺少一些很有用的库如 XML API,socket,GUI、反射——以及自动垃圾收集。然而现有特性已经让 C++ 更安全、高效(是的,效率更高了,可以参见 Google 的 基准测试结果http://www.itproportal.com/2011/06/07/googles-rates-c-most-complex-highest-performing-language/)以及更加易于学习和使用。


如果觉得 C++ 变化太大了,不必惊恐,花点时间来学习就好了。可能在你融会贯通新特性以后,你会同意 Stroustrup 的观点:C++11 是一门新的语言——一个更好的 C++。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 地板  发表于: 2011-07-04
C++老矣,尚能饭否?
中国人读“C++”时存在两种读音,一种读作“C加加”,另一种读作“C plus plus”。两种读音似乎都没错,只是遵从不同的标准罢了。如同C++有两种读法一样,目前C++阵营甚至没有一个完整的标准,只有最符合和接近C++标准的编译器GNU GCC 4.6.1和微软的Visual Studio 2010 SP1。C++这个在Tiobe编程语言排行榜中跻身三甲的语言,为何连完整的标准都没有?





C++之父 Bjarne Stroustrup


C++之父Bjarne Stroustrup的简历上只有一句话“C++缔造者”,这样的成就是荣耀无比的。但似乎C++似乎过于低调,让很多C++程序员都没有新消息可以接收。Java有Oracle等大公司推动、.NET有微软推动、HTML 5有Google等推动、包括PHP有众多用户和社区推动,那C++有谁在推动呢?


C++从20世纪80年代面世,经历了以下三个阶段的发展


第一阶段从1980年代到1995年。这一阶段C++语言基本上是传统类型上的面向对象语言,并且凭借着接近C语言的效率,在工业界使用的开发语言中占据了相当大份额;


第二阶段从1995年到2000年,这一阶段由于标准模板库(STL)和后来的Boost等程序库的出现,泛型程序设计在C++中占据了越来越多的比重性。当然,同时由于Java、C#等语言的出现和硬件价格的大规模下降,C++受到了一定的冲击;


第三阶段从2000年至今,由于以Loki、MPL等程序库为代表的产生式编程和模板元编程的出现,C++出现了发展历史上又一个新的高峰,这些新技术的出现以及和原有技术的融合,使C++已经成为当今主流程序设计语言中最复杂的一员。





Borland已陨落


相信很多程序员跟51CTO记者一样,在大学里学习过《C++语言程序设计基础》。听着老师们指着谭浩强的《C++程序设计》,不厌其烦的讲解char、int等数据类型的取值范围,指针指向地址取值的问题。在这过程中我们接触的第一个IDE(编译器)是Borland公司的C++ Builder,尽管他已经沦落到被人收购的境地。2009年5月6日,英国软件商Micro Focus宣布,公司将以7500万美元现金收购Borland软件公司,一代软件开发工具先驱者陨落了。


不管我们用的是Borland公司还是微软的VC环境,很少有程序员关心C++的标准和版本问题。我们经常能听到Java或.NET程序员谈论Java 7或者.NET 3.5等版本的新特性,但有谁关心过C++的新版本?


51CTO记者随机对一些C++程序员进行了调查,有50%的程序员表示对开发语言的新版本有兴趣,但不会用到实际工作中。只有当所在公司要求他们统一进化到新版本时,才会使用新版本。有10%的程序员会从繁忙的工作中抽出时间来学习新版本,并独自运用到开发工作中。剩下40%的程序员表示天天加班,根本没有时间看所谓的新版本,到时候现学吧。


这样的调查结果让我们了解了国内程序员对编程语言新版本的态度。从51CTO发布的新闻《掀起C++ 11的神秘面纱》中,我们听到了不少C++的程序员对于C++ 11的悲观论调,甚至预言未来的C++ 11进不了编程语言排行榜的前十。


C++真的老了?


现在最好找工作的程序员是哪一类?根据51CTO编辑在智联招聘网站,以北京地区为范围,搜索到不同开发语言工作岗位数量如下:


PHP    5212
Java    11824
C++    9252
ASP.NET    2383
可以看出C++程序员的需求是旺盛的,甚至超越了因电子商务而红火的PHP程序员。C++并不因为版本的问题而遭到抛弃,廉颇老矣,尚能饭否的应该是VB这样的语言。因为在智联招聘里,VB这个关键词只有可怜巴巴的827个职位。


看来C++还是很多程序员的饭碗,大家一致用行动证明C++并没有老去,并没有被人所遗忘。只是我们身在C++中,并不太关心它的进步和发展,只要用C++能处理好手头上的工作,哪管C++ 11的未来?


为什么新手不选择C++?


很多新人喜欢Java或者.NET,因为他们开发起来相对简单,甚至说高效。以往C++的优势在于对硬件资源的合理分配,特别是代码行超过三万以后,C++的系统消耗优势更加明显,这也是为什么很多大公司热衷于C++做大型开发的原因。


但随着内存和CPU等硬件设备的降价,硬件资源和性能问题似乎不再成为程序员们考虑的问题。就好像386、486时代的PC用户还要为那几十K的内存优化费劲一样,C++程序员那穷尽极致的节约系统资源,已经被看做是费力不讨好,新手们自然不选择C++了。


其实百度和《魔兽世界》都是C++写出来的。


看C++ 11的新特性


C++没有垃圾回收机制,未来似乎也没有。如果你不小心没管住内存,那么就要对不起了,内存崩溃的情况不是不可能。在Java和.NET都支持垃圾回收之后,C++ 11还是显得有些另类。当然我们也是猜测,这样对内存的精确控制,是不是出于对程序开发更加精确的考虑?


新的C++ 11里将会支持多线程开发,这个与目前多核CPU技术的发展密切相关,能极大的提高C++开发成品的效率。这个新特性应该是与时俱进的改变,还是没有多少创新性。Visual Studio 2010也已经实现C++并行构建的功能。


Lambda表达式也是此次C++ 11标准中最为人诟病的特性,甚至有C++程序员直言“这Lambda太丑陋了,还不如Java,尽管Java的Lambda也是极其丑陋。”也有清醒的C++程序员认为Lambda表达式在大型项目中的应用还是不错的,新的语言机制会带来新的效率,尽管这会有些阵痛。关于Java 7与C#中Lambda表达式的优劣,请点击这里。


借用一位C++程序员的例子来说明Lambda的正面作用,在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象,


class  filler  
  {  
public :  
  void   operator ()( bool   & i)  const    {i  =   true ;}  
} ;  
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决:


for_each(v.begin(), v.end(), _1  =   true );
面对Java和.NET这样的后起之秀,C++的转身似乎显得慢了一些。在移动开发和WEB应用红火的今天,C++的声音似乎小了一些,希望这次C++ 11的新闻不会被Java 7和HTML 5给淹没了。


C++ 11标准放出,骂的人比捧的人要多。一是认为原有的C++老标准已经满足需要,二是认为新标准加入的新功能让初学者更不能适应。这两种意见有道理,就如同Windows XP已经很方便了,Windows 7全新的操作让很多人不适应一样。





搞C++要有革命热情


一切只能等C++ 11真正出台,让更多的C++程序员用到C++ 11才能有说服力。仅凭媒体放出的一些新特性恐怕还不足以让大家完全了解C++ 11的真面目。51CTO编辑认为,C++ 11能把C++这款“老”编程语言带到一个新的高度,C++还会是排名三甲的编程语言。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 4楼 发表于: 2011-07-04
掀起C++ 11的神秘面纱
C++之父Bjame Stroustrup最近说C++11就像一个新语言,的确,C++11核心已经发生了巨大的变化,它现在支持Lambda表达式,对象类型自动推断,统一的初始化语法,委托构造函数,deleted和defaulted函数声明nullptr,以及最重要的右值引用。





【编辑推荐】


C++0x FAQ中文版(http://imcc.blogbus.com/logs/106046323.html


C++11标准库也使用新的算法,新的容器类,原子操作,类型特征,正则表达式,新的智能指针,async()函数和多线程库进行了改造。


C++11的新内核和库特性完整列表请移步这里(http://www2.research.att.com/~bs/C++0xFAQ.html)。 C++标准在1998年获得通过后,有两位委员会委员预言,下一代C++标准将“肯定”包括内置的垃圾回收器(GC),但可能不会支持多线程,因为定义一个可移植的线程模型涉及到的技术太复杂了,13年后,新的C++标准C++11也接近完成,你猜怎么着?让那两位委员没想到的是,本次更新还是没有包括GC,但却包括了一个先进的线程库。


在这篇文章中,我将介绍C++11标准中发生的最大变化,以及为什么应该引起注意,正如你将看到的,线程库不是唯一的变化,新标准采纳了数十位专家的意见,使C++变得更有意义。正如Rogers Cadenhead指出的那样,它们就像迪斯科、宠物石和长胸毛的奥运游泳选手一样不可思议。


首先,让我们看看C++11核心语言的一些突出特性。


Lambda表达式


Lambda表达式允许你在本地定义函数,即在调用的地方定义,从而消除函数对象产生的许多安全风险,Lambda表达式的格式如下:


[capture](parameters)->return-type {body}
[]里是函数调用的参数列表,表示一个Lambda表达式的开始,让我们来看一个Lambda例子:


假设你想计算某个字符串包含多少个大写字母,使用for_each()遍历一个char数组,下面的Lambda表达式确定每个字母是否是大写字母,每当它发现一个大写字母,Lambda表达式给Uppercase加1,Uppercase是定义在Lambda表达式外的一个变量:


int main()
  
{
  
   char s[]="Hello World!";
  
   int Uppercase = 0; //modified by the lambda
  
   for_each(s, s+sizeof(s), [&Uppercase] (char c) {
  
    if (isupper(c))
  
     Uppercase++;
  
    });
  
cout<< Uppercase<<" uppercase letters in: "<< s<<endl;
  
}
这是因为,如果你定义的函数主体被放置在另一个函数调用内部,[&Uppercase]中的“&”记号意味着Lambda主体获得一个Uppercase的引用,以便它能修改,如果没有这个特殊记号,Uppercase将通过值传递,C++11 Lambda表达式也包括成员函数构造器。


自动类型推断和decltype


在C++03中,在声明对象时,你必须指定对象的类型,然而,在许多情况下,对象的声明包括在初始化代码中,C++11利用了这个优势,允许你声明对象时不指定类型:


auto x=0; //x has type int because 0 is int
  
auto c='a'; //char
  
auto d=0.5; //double
  
auto national_debt=14400000000000LL;//long long
相反,你可以声明下面这样的迭代器:


void fucn(const vector<int> &vi)
  
{
  
vector<int>::const_iterator ci=vi.begin();
  
}
  
关键字auto不是什么新生事物,我们早已认识,它实际上可以追溯到前ANSI C时代,但是,C++11改变了它的含义,auto不再指定自动存储类型对象,相反,它声明的对象类型是根据初始化代码推断而来的,C++11删除了auto关键字的旧有含义以避免混淆,C++11提供了一个类似的机制捕捉对象或表达式的类型,新的操作符decltype需要一个表达式,并返回它的类型。


const vector<int> vi;
  
typedef decltype (vi.begin()) CIT;
  
CIT another_const_iterator;
  
统一初始化语法


C++至少有4个不同的初始化符号,有些存在重叠,括号初始化语法如下:


std::string s("hello");
  
int m=int(); //default initialization
  
在某些情况下,你也可以使用“=”符号进行初始化:


std::string s="hello";
  
int x=5;
  
对于POD聚合,你还可以使用大括号:


int arr[4]={0,1,2,3};
  
struct tm today={0};
  
最后,构造函数使用成员进行初始化:


struct S {
  
int x;
  
S(): x(0) {} };
  
显然,这么多种初始化方法会引起混乱,对新手来说就更痛苦了,更糟糕的是,在C++03中,你不能初始化POD数组成员,POD数组使用new[]分配,C++11使用统一的大括号符号清理了这一混乱局面。


class C
  
{
  
int a;
  
int b;
  
public:
  
C(int i, int j);
  
};
  


  
C c {0,0}; //C++11 only. Equivalent to: C c(0,0);
  


  
int* a = new int[3] { 1, 2, 0 }; /C++11 only
  


  
class X {
  
  int a[4];
  
public:
  
  X() : a{1,2,3,4} {} //C++11, member array initializer
  
};
关于容器,你可以和一长串的push_back()调用说再见了,在C++11中,你可以直观地初始化容器:


// C++11 container initializer
  
vector vs<string>={ "first", "second", "third"};
  
map singers =
  
  { {"Lady Gaga", "+1 (212) 555-7890"},
  
    {"Beyonce Knowles", "+1 (212) 555-0987"}};
  
类似地,C++11支持类似的数据成员类内初始化:


class C
  
{
  
int a=7; //C++11 only
  
public:
  
C();
  
};
  


Deleted和Defaulted函数


一个表单中的函数:


struct A
  
{
  
A()=default; //C++11
  
virtual ~A()=default; //C++11
  
};
  
被称为一个defaulted函数,“=default;”告诉编译器为函数生成默认的实现。Defaulted函数有两个好处:比手工实现更高效,让程序员摆脱了手工定义这些函数的苦差事。


与defaulted函数相反的是deleted函数:


int func()=delete;
Deleted函数对防止对象复制很有用,回想一下C++自动为类声明一个副本构造函数和一个赋值操作符,要禁用复制,声明这两个特殊的成员函数=delete即可:


struct NoCopy
  
{
  
    NoCopy & operator =( const NoCopy & ) = delete;
  
    NoCopy ( const NoCopy & ) = delete;
  
};
  
NoCopy a;
  
NoCopy b(a); //compilation error, copy ctor is deleted
  
nullptr


C++终于有一个关键字指定一个空指针常量了,nullptr取代了有错误倾向的null和文字0,这两个被用来作为空指针替代品已经有很多年的历史了,nullptr是一个强类型:


void f(int); //#1
  
void f(char *);//#2
  
//C++03
  
f(0); //which f is called?
  
//C++11
  
f(nullptr) //unambiguous, calls #2
  
nullptr适用于所有指针类别,包括函数指针和成员指针:


const char *pc=str.c_str(); //data pointers
  
if (pc!=nullptr)
  
  cout<<pc<<endl;
  
int (A::*pmf)()=nullptr; //pointer to member function
  
void (*pmf)()=nullptr; //pointer to function
  
委托构造函数


在C++11中,构造函数可以调用相同类中的其它构造函数:


class M //C++11 delegating constructors
  
{
  
int x, y;
  
char *p;
  
public:
  
M(int v) : x(v), y(0),  p(new char [MAX])  {} //#1 target
  
M(): M(0) {cout<<"delegating ctor"<
  
构造函数#2,委托构造函数,调用目标构造函数#1。


右值引用


C++03中的引用类型只能绑定左值,C++11引入了一种新型引用类型,叫做右值引用,右值引用可以绑定左值,例如,临时对象和字面量。增加右值引用的主要原因是move(移动)语义,它和传统的复制不一样,移动意味着目标对象偷窃了源对象的资源,留下一个状态为“空”的资源,在某些情况下,复制一个对象代价既高又没有必要,可以用一个移动操作代替,如果你想评估移动带来的性能收益,可以考虑字符串交换,一个幼稚的实现如下:


void naiveswap(string &a, string & b)
  
{
  
string temp = a;
  
a=b;
  
b=temp;
  
}
  
像这样写代价是很高的,复制字符串必须分配原始内存,将字符从源位置复制到目标位置,相反,移动字符串仅仅是交换两个数据成员,不用分配内存,复制char数组和删除内存:


void moveswapstr(string& empty, string & filled)
  


{
  


//pseudo code, but you get the idea
  


size_t sz=empty.size();
  


const char *p= empty.data();
  


//move filled's resources to empty
  


empty.setsize(filled.size());
  


empty.setdata(filled.data());
  


//filled becomes empty
  


filled.setsize(sz);
  


filled.setdata(p);
  


}
  


如果你实现的类支持移动,你可以像下面这样声明一个移动构造函数和一个移动赋值操作符:


class Movable

{

Movable (Movable&&); //move constructor

Movable&& operator=(Movable&&); //move assignment operator

};





C++11标准库广泛使用了移动语义,许多算法和容器现在都为移动做了优化。


C++11标准库


C++于2003年以库技术报告1(TR1)的形式经历了重大改版,TR1包括新的容器类(unordered_set,unordered_map,unordered_multiset和unordered_multimap)和多个支撑正则表达式、元组和函数对象封装器等的新库。随着C++11标准获得通过,TR1和自它首次发布以来新增的库被正式纳入标准的C++标准,下面是C++11标准库的一些特性:


线程库


站在程序员的角度来看,C++11最重要的新功能毫无疑问是并行操作,C++11拥有一个代表执行线程的线程类,在并行环境中用于同步,async()函数模板启动并行任务,为线程独特的数据声明thread_local存储类型。如果你想找C++11线程库的快速教程,请阅读Anthony William的“C++0x中更简单的多线程”。


新的智能指针类


C++98只定义了一个智能指针类auto_ptr,它现在已经被废弃了,C++11引入了新的智能指针类shared_ptr和最近添加的unique_ptr,两者都兼容其它标准库组件,因此你可以在标准容器内安全保存这些智能指针,并使用标准算法操作它们。


新的算法


C++11标准库定义了新的算法模仿all_of(),any_of()和none_of()操作,下面列出适用于ispositive()到(first, first+n)范围,且使用all_of(), any_of() and none_of() 检查范围的属性的谓词:


#include <algorithm>

//C++11 code

//are all of the elements positive?

all_of(first, first+n, ispositive()); //false

//is there at least one positive element?

any_of(first, first+n, ispositive());//true

// are none of the elements positive?

none_of(first, first+n, ispositive()); //false

一种新型copy_n算法也可用了,使用copy_n()函数,复制一个包含5个元素的数组到另一个数组的代码如下:


#include

int source[5]={0,12,34,50,80};

int target[5];

//copy 5 elements from source to target

copy_n(source,5,target);

算法iota()创建了一个值顺序递增的范围,好像分配一个初始值给*first,然后使用前缀++使值递增,在下面的代码中,iota()分配连续值{10,11,12,13,14}给数组arr,并将{‘a’,’b’,’c’}分配给char数组c。


include <numeric>

int a[5]={0};

char c[3]={0};

iota(a, a+5, 10); //changes a to {10,11,12,13,14}

iota(c, c+3, 'a'); //{'a','b','c'}

C++11仍然缺乏一些有用的库,如XML API,套接字,GUI,反射以及前面提到的一个合适的自动垃圾回收器,但C++11的确也带来了许多新特性,让C++变得更加安全,高效,易学易用。


如果C++11的变化对你来说太大的话,也不要惊慌,多花些时间逐渐消化这一切,当你完全吸收了C++11的变化后,你可能就会同意Stroustrup的说法:C++11感觉就像一个新语言,一个更好的新语言。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 5楼 发表于: 2011-08-12
C++0x 热点问题访谈-2008

2004年底前后,经过较长一段时间的沉默,一批世界级的C++著作相继面世。2005年4月,在挪威Lillehammer举行的C++标准委员会会议上,Bjarne Stroustrup促成委员会达成一致意见:让C++0x中的x等于9。2005年11月,Bjarne Stroustrup、Herb Sutter、Stanley B. Lippman、Andrei Alexandrescu等前辈、新锐将在Las Vegas庆祝C++ 廿周年。2005年底,C++中国社群将在上海举办首届“现代C++设计和编程”技术大会……C++好戏连台,令人振奋。笔者近日就C++0x以及其他一些热点问题请教了Bjarne先生。大师观点,不敢专美,整理成文,以飨同好。


C++0x
荣耀:Library TR1(library Technical Reports,库技术报告)和TR2的动机是什么?TR1和TR2、TR1/TR2和C++0x、Performance TR(Performance Technical Reports,性能技术报告)和C++0x、Boost和C++0x之间的关系如何?
Bjarne:Library TR是对标准库改进工作的具体结果的体现。当一套设施、特性就绪后,TR1即被表决通过,然后人们继续向TR2前进。TR1和TR2之间的区别仅在于“时间不同,所做的事情不同”而已。大部分TR1和TR2中的内容有望成为C++0x的一部分。
Performance TR是一个关于C++适合于性能严苛和资源受限的编程的报告,大多和嵌入式系统编程有关。其作用主要在于教育程序员并努力驱散萦绕C++的能与不能的流言蜚语。特别要指出的是,该TR证明C++是一门极好的适合于嵌入式系统编程的语言。
启动Boost项目的人们(著名的如Beeman Dawes)过去是、现在仍然是C++标准委员会的成员。Boost的目标是提供对标准库的扩展,并使得大多数有用且成功的Boost库成为新一代C++标准。其中一部分(而非全部)将会成为C++0x的一部分。注意,当一个库被添加进标准时,往往需要对其进行某种程度的修改。许多TR1扩展一开始都是作为Boost的一个组成部分而开始它们的生命旅程的。
荣耀:我们经常听到C++在不损及效率的前提下达成抽象和优雅,在C++0x的演化(设计)中同样如此,您能从技术的层面来谈谈C++(0x)是如何做到这一点的吗?
Bjarne:这个问题说来话长,需要专门写一篇论文。不过基本的思想一如既往:遵从零开销原则(参见D&E),拥有内建的直接到硬件的操作映射,以及提供强大灵活的静态类型系统。




[table=medium][tr][td=1,1,568]荣耀注:D&E是Bjarne写的一本关于C++语言设计原理、设计决策和设计哲学的专著。该书全名是《The Design and Evolution of C++》。零开销原则的含义是:无需为未使用的东西付出代价。详情参见D&E。
[/td][/tr][/table]
荣耀:您最近提到在C++0x标准化过程中,委员会奉行的若干原则中有这样一条:只做可以改变人们思考方式的改变,请问此原则的出发点是什么?
[table=medium][tr][td=1,1,568]荣耀注:D&E中并没有明确地提及这条设计原则,但Bjarne在最近的一篇文章里特别提到了这一点,故有此一问。
[/td][/tr][/table]
Bjarne:任何改变都会带来在实现、学习等方面的代价。对一个程序员编写具体哪一行代码的方式的改进不会带来太大的好处,能够改进程序员解决问题和组织程序的方式才可以。面向对象和泛型编程已经改变了很多人的思考方式,这也是很多C++语言设施支持这些风格的用意。因此,对于语言和库设计者来说,最好将时间花在有助于改变人们思考方式的设施和技术之上。
荣耀:如果让您分别列出可能会进入C++0x标准的Top 5语言新特性和库扩展,您愿意列出哪些?
Bjarne:语言特性包括:
²         Concepts
²         一般化的初始化(generalized initialization)
²         对泛型编程的更好的支持(auto、decltype等)
²         内存模型
²         ……
[table=medium][tr][td=1,1,568]荣耀注:Concepts在泛型编程中具有极其重要的地位,但C++98并未对concepts提供直接的支持,它们只是以文档的形式连同一套松散的编程约定而存在。目前STL中的concept的作用仅限于在模板实例化时对模板参数进行检查,然而,如果某个函数模板参数不符合某个concept,编译器并不能对该函数模板的定义进行及早的强类型纠错。尽管Bjarne先生一向对语言扩展持保守态度,但在为concept提供语言层面的直接支持方面却一向积极,实际上,他本人正是为C++加入concept的最早的提案人之一。几乎可以肯定该语言特性会被加入到C++0x之中。
[/td][/tr][/table][table=medium][tr][td=1,1,568]荣耀注:关于“一般化的初始化”问题可参见后面的问答。内存模型也就是后面所说的机器模型。auto和decltype则分别是计划加入C++0x的新关键字和操作符,它们均可用于改善泛型编程。
[/td][/tr][/table]


库设施则有:
²         unordered_maps(即哈希表)
²         正则表达式匹配
²         线程
²         ……
我不愿意勉强向这种“top N”列表中添加更多的东西,关于还有哪些主要的东西应该被添加进来,目前尚存在激烈的讨论。
荣耀:您说过,“在C++0x的concept的设计中存在的最大的困难是维持模板的灵活性。我们不要求模板参数适合于类层次结构或要求所有操作都能够通过虚函数进行访问(就象Java和C#的“generics”所做的那样)”。可以详谈一下吗?
Bjarne:在“generics”中,参数必须是派生于在generic定义中所指定的接口(C++中的等价物则是“抽象类”)。这就意味着所有generic参数类型必须适合于某个类层次结构。举个例子,如果你编写了一个generic,我定义一个class,人们无法使用我的class作为你的generic的参数,除非我知道你指定的接口,并使该class派生于它。这种做法太呆板。当然了,对此问题有迂回解决办法,但需要编写复杂的代码。
另一个问题是,因为内建的类型(比如int)不是类,它们没有generics所使用的接口所要求的函数,因此你不得不制作一个包装器类来容纳内建的类型。
还有,一个generic的典型操作是以虚函数调用的方式实现的。与仅使用一个简单的内建操作(例如+或<)相比,这种做法可能代价高昂。Generics的实现方式表明它们不过是抽象类的语法糖而已。
[table=medium][tr][td=1,1,568]荣耀注:请注意,这并不是对Java/C# Generics的“全面”的描述,对C# 2.0 generics而言尤其不适合。
[/td][/tr][/table]




荣耀:您说过,C++0x极有可能通过“一个机器模型外加支持线程的标准库设施”来支持并发访问,请解释一下您所指的机器模型是什么?
Bjarne:当程序只有一个执行线程并且表达式都是按照程序员编写的顺序正确地执行时,处理内存是件很简单的事情:我们只管读写变量,无需想得太多。我们希望在今天更复杂的机器(具有高速缓存、宽数据总线以及多处理器)上仍然可以如此。一个内存模型是一套规则:它告诉编译器作者必须确保哪些东西在所有机器上都得到保证。
考虑
char x;
char y;
如果某个多线程程序的一个线程在递增x,另一个线程在递增y,我们当然希望两个字符变量均被正确地递增。然而,在很多现代机器上,我们无法在寄存器和高速缓存之间或高速缓存和内存之间正好传递8个比特。因此,如果这两个字符都分配在同一个字(word)上,我们就无法做到在读写x时不去读写y。
可能还存在更多的高速缓存(每一个都持有一份x和一份y的拷贝)。基本上,一个优秀的内存模型可以使程序员无需关心此类机器问题——这些问题留给编译器作者去操心,为程序员提供的总是显而易见的语言语义。
荣耀:您说过,C++0x的高级目标之一是使C++更易于教和学。通过“增强的一致性、更有力的保证”来做到这一点,您能否给出这方面的一些例子?
Bjarne:比如说,目前的初始化规则太复杂且不规则。考虑一下在哪儿可以使用“={initializers}”、哪儿可以使用“(initializers)”、哪儿可以使用“=initializer”吧。我希望通过允许当前不被允许的用法,使得程序员只需学习和记住较少的规则,从而达到简化这个领域的目的。这方面的工作集中于实现“{initializers}”语法。
[table=medium][tr][td=1,1,568]荣耀注:举个例子。在现有的C++中,我们这样来“初始化”一个vector:
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

采用“{initializers}”语法,我们就可以消除对push_back()的重复调用。如下:
vector<int> v = { 1, 2, 3 };
[/td][/tr][/table]


一个“更有力的保证”的例子,如前所述,精确地定义一个内存模型从而使得并发程序更具有可移植性。
我还希望看到针对所有标准容器的越界检查的标准方式。这种有益的保证的例子之一是:“如果你对任何标准容器的任何访问是越界的,那么就会抛出一个异常”。
……
总而言之,关键的思想在于使得广泛的常用语句变得更易于学习和使用。
荣耀:顺带一问,您是否认为目前的泛型代码对于普通用户来说太难以理解了?
Bjarne:是的。很多泛型代码和模板元编程代码太难以理解了,而且,对于哪怕很微小的用户错误给出的错误消息也非常难以理解。
要想使得模板定义成为主流,我们必须使得模板更容易编写和使用。当前的很多用法过于聪明。优秀的代码应该是简单的(相对于现状而言),并且易于检查,易于优化(即高效)。
荣耀:C++0x会支持元数据和反射吗?此外,你对Boost.Serialization库又怎么看?
Bjarne:C++0x不太可能支持元数据。我对Boost.Serialization库不予置评。
荣耀:为何C++0x无意支持元数据和反射?看上去它是不少C++程序员“梦寐以求”的特性。
Bjarne:很多程序员梦寐以求很多语言特性,如果C++将它们统统采纳进来,C++就会因为过度膨胀而失去用处。元数据这样的语言特性需要付出较多的工作,已经超出了标准委员会的能力。而且我并不认为元数据很好地适合于C++对“静态类型以及在concepts和types之间的直接对应”的强调。元数据可以被用于隐藏“走访”数据结构(元数据)的例程中的程序的真实语义,我对此类用法的正确性和性能尚持怀疑态度。
荣耀:在Unicode、XML以及网络方面,有没有C++0x标准库的好的候选者?
Bjarne:关于Unicode的工作有在做,但主要是C风格的。除了线程和sockets外,尚无严肃的关于XML、网络方面的工作。虽然应该进行这方面的工作,但标准委员会好像没有足够的资源来做这些事。这儿的资源指的是“有时间、有兴趣的技术能手”。
荣耀:一些人认为当前C++对TMP(Template Metaprogramming,模板元编程)的支持不够好,就像C对OOP的支持那样,仅仅勉强可行,您是否赞同这一点?为了更好地支持TMP,您认为还需要向C++0x中添加什么样的特性?
Bjarne:就目前来看,C++对TMP的支持似乎比任何语言都要好。毋庸置疑,我们可以改进语言使其更好地支持TMP,问题是我们到底该不该这么做?什么样的做法才算是一种改善?什么样的程序最好执行于编译期?什么样的程序又应该采用通用语言特性进行实现?没错,我们是可以设计一种专用的语言,与C++相比它对TMP有着更好的支持,就像我们可以为任何特殊的任务设计一种专用的语言那样。问题还是那句话:这样做值得吗?
在C++0x(标准库)的设计中,中心议题仍然是经典的泛型编程。
荣耀:一个关于TMP的小问题。C++98标准建议编译器实现至少要支持17层递归模板实例化,但由于这只是一个“建议”而非“规定”,一个即使只支持1层递归实例化的编译器也是遵从标准的,不是吗?换句话说,TMP的重要基础之一——递归模板实例化并未得到标准更为“名正言顺”的支持——这项技术是和特定编译器有关的。C++0x有意增强这一点吗?
Bjarne:我不知道,不过17这个数字也太小了。


[table=medium][tr][td=1,1,568]荣耀注:实际上,我们常用的编译器对递归模板实例化的层数支持远大于17层,一般可达数百乃至数千层。
[/td][/tr][/table]
荣耀:考虑到要添加现有C++98编译器所不支持的新语言特性,标准委员会在进行C++0x标准化工作的过程中,都采用哪些编译器来测试(验证)新语言特性?
Bjarne:有很多编译器可以用来测试新语言特性。所有程序员都可以使用GCC进行尝试,学院人士可以使用EDG。所有编译器厂商都在他们的编译器中试验新语言特性。
[table=medium][tr][td=1,1,568]荣耀注:EDG(Edison Design Group)是一家专注于开发C/C++、Java以及Fortran 77编译器front end(用于将源代码翻译成某种中间语言,然后再由back end将中间语言翻译成机器码)的公司。Intel C++和Comeau C/C++等许多编译器都使用了EDG front end。
[/td][/tr][/table]
荣耀:一个有点儿八卦的问题。C++标准委员会中有中国人吗?有中国人向C++标准委员会递交过提案吗?
Bjarne:我想不起来最近的提案和中国人有关。委员会中有一个IBM的新代表,姓王。我猜他是中国人,但我还不认识他。
考虑到中国有那么多人在从事计算机工作,我一直都很奇怪为什么看不到你们对C++0x标准化工作的参与。
其他
荣耀:和运行期C++相比,编译期C++、尤其是模板元编程的优点何在?TMP最适合用于哪些场合?TMP已经成功地应用于哪些类型的应用中了?
Bjarne:对于这一个问题我还没有确定的答案。我认为TMP主要适合于相对简单的编译期计算,即用于选择执行于运行期的代码,而不是用于获得数值结果的复杂的计算,例如计算阶乘或质数。在这样的场合下,TMP充当“高级的”#ifdef。


[table=medium][tr][td=1,1,568]荣耀注:“计算质数”意思是求出小于给定正整数的所有质数。
[/td][/tr][/table]
荣耀:是的。所以像Boost Type Trait这样的库已经被写进了Library TR。另外,我认为Blitz++这样的库对于科学计算来说还是非常有意义的。
Bjarne:对。
荣耀:一个基础的问题。为什么在TMP中不能使用浮点型数据?这儿存在什么技术上的困难吗?我没看到C++0x有支持编译期浮点型数据计算的迹象,您知道,这对于编译期数值计算来说还是很有意义的。
Bjarne:浮点计算不是百分之百精确,我们不能依赖于它的计算结果进行比较……
荣耀:看上去在编译期C++和运行期C++之间存在着一道鸿沟,我们如何修补它?抑或如何将二者结合使用?
Bjarne:问题仍然是:我们应该修补它吗?我们能从对TMP的显著改善中获得什么样的好处?
荣耀:您说过C++是一门“偏向于系统编程的通用语言”。“系统编程”是一个宽泛的概念,不同的人有着不同的理解,您的意思是?
Bjarne:当我说系统编程时,我是指传统上与操作系统以及基础工具有关的编程任务。包括操作系统核心、设备驱动程序、系统工具、网络应用、编辑器、字处理工具、编译器、某些图形和GUI应用,以及数据库系统等。这类工作在当前的C++用户中占有主导地位(参见我的个人主页上的“Applications”单元)。


荣耀:尽管我们都知道在C++中面向对象和泛型编程同等重要,但是,您是否和大多数人一样,认为C++对泛型编程的强力支持是它与其他语言的显著区别?
Bjarne:对泛型编程的强力支持只是显著的区别和显著的优势之一,即便与那些提供了“generics”的语言相比也是如此。一如既往,C++的主要力量不在于在某一方面表现完美,而是擅长于很多事情,因此,对于许多应用领域来说,C++都是一个优秀的工具。
荣耀:看上去C++程序的编译速度要比Java或C#的来得慢,原因何在?当然了,我们可以想象语言自身的复杂性以及模板的实例化(即使程序员没有显式地使用模板,但他所使用的库比如STL,也使用了模板)等因素使然。
Bjarne:Java和C#这样较新的语言,其语言结构较为简单。更重要的是,它们不像C++这样在编译期做很多事情。形形色色的编译期评估是造成慢的一个主要原因,不过编译时间被拉长带来的好处往往是程序运行期性能的提高。还有一个原因值得一提,连接时间往往也被认为是构成C++编译时间一个不小的组成部分。
荣耀:多年以来——今天仍然如此,有很多人在讨论什么是“高级C++”,您能谈谈什么是高级C++吗?C++/高级C++的明天是什么样子?
Bjarne:在我看来,人们在为了变得更聪明、更高级方面用力过度了。设计和编程的真正目标在于产生能够完成工作的最简单的解决方案,并且对该解决方案的表达要尽可能的清晰。理想的境界不是让那些看到你代码的人惊呼“哇塞,好聪明耶!”,而是“哈哈,这么简单?”
我认为这样的简单代码将会包含大量的相对简单的泛型编程,同时带有一些类继承层次结构,后者为需要运行期解决方案的领域提供服务。用今天的话来说,此即为“多范型编程(multi-paradigm programming)”,但我们无疑需要为之寻找一个更好的术语。


参考资源
1.      The C++ Standards WG, http://www.open-std.org/jtc1/sc22/wg21/
2.      Bjarne Stroustrup, The Design and Evolution of C++, Addison-Wesley
3.      Bjarne Stroustrup, The Design of C++0x, C/C++ Users Journal (May 2005)
4.      Bjarne Stroustrup homepage, http://www.research.att.com/~bs
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 6楼 发表于: 2011-08-14
C++0x探秘:访C++之父Bjarne Stroustrup
C++0x作为C++的下一个国际标准,已经在业界热炒多年。但是,尽管业界对这个新标准千呼万唤,她总是不愿意过早地来到我们面前。在最近一次CodeGuru对C++之父Bjarne Stroustrup博士的采访中,C++之父终于给我们带来了好消息——C++0x的标准化工作已经接近尾声,C++0x呼之欲出。下面是整个采访过程的节选,我们可以通过这个访谈,掀起C++0x的盖头来,了解C++0x的最新进展,新的特性以及未来的计划。
  Danny Kalev: C++0x的标准化过程进展如何?我们现在有多么接近这个新的C++标准?
  Bjarne Stroustrup: 我们计划在2011年3月26日进行最终的技术投票。虽然其后还会进行正式的国家投票以及ISO官僚主义的拖延,但是我十分相信,我们会在2011年使这个官方的标准来到我们面前。
  Danny Kalev: 在所有关于C++0x的核心特性以及库的改进中,C++0x为一个典型的C++程序员带来了哪些好东西?C++0x的哪些方面使您特别骄傲?最后,鉴于目前市面上还缺少关于C++0x的相关教材和资料,对于程序员们学习和使用C++0x的新特性您有什么建议?
  Bjarne Stroustrup: C++0x对于C++的改进是以许多小的语言特性的改进以及部分新特性的增加的形势出现的,并不是对C++革命性的更新。我猜想,许多改进对于大多数人而言并不是十分重要的,但是会让C++变成一门更好的程序设计语言。但是这一点也并不会影响我的核心观点:C++0x的改进弥散地分布在C++语言的各个部分;它们在许多地方以多种形式改善我们的C++代码;并不像人们通常理解的改进一样,被隔离成某一个独立的新增加的组件。更形象地说,我认为C++0x的改进就像我们获得了很多新的种类的砖块,这样我们可以构建很多以前无法轻松构建的建筑,并且更加容易和灵活优雅。实际上,使用C++0x,我总是能够写出比使用C++98更加简单更加优雅的程序,并且,通常也会有更高的性能。
  当我们首先来看几个可以让C++程序员的生活更加轻松惬意的C++0x改进。考虑下面这段代码:
  
void f(vector<pair<string,int>>& vp)
          {
              struct is_key {
                  string s;
                  bool operator()(const pair<string,int>&p)
                      { return p.first == s; }
              };
              auto p = find_if(vp.begin(), vp.end(), is_key{"simple"});
              // …
                  }
                  
  这段代码看起来并没有什么激动人心的新改进,但是我要指出其中有四个小特性是C++98所不具备的:
  1. 在“vector>”中,第一个“>”和第二个“>”之间并没有空格,我认为这是C++0x中最小的改进,但是却可以省去程序员们添加空格的繁琐。
  2. 我定义了一个变量p但是并没有明确地指出它的数据类型,作为替代,我使用了auto作为其数据类型,这就意味着“使用初始器(initializer)的数据类型”,所以p的数据类型就是vector>::iterator。这将节省程序员编码以及调试可能出现的Bug的时间。这是C++0x最“老”的新特性,我在1983年就实现了这个特性,但是因为一些兼容性问题,这一特性一直没有被纳入C++标准。
  3. 局部结构体is_key被用作模板参数类型,也许你并没有注意到这一点,但是这样的用法在C++98中是非法的。
  4. 最后,我使用初始器{"simple"}创建了一个键。在C++98中,我们只能够以这样的方式初始化一个变量而不能初始化一个函数参数。C++0x通过“{...}”操作符,提供了一致的初始化方式。
  我们还可以利用Lambda表达式进一步简化这个例子:


   void f(vector<pair<string,int>>& vp)
          {
          auto p = find_if(vp.begin(), vp.end(),
              []()(const pair<string,int>&p) { return p.first=="simple"; });
          // …
                  }
                  
  Lambda表达式是对函数对象的定义和使用的一种简化。这里,我们使用Lambda表达式简单地表示了find_if()算法的谓语使用pair作为参数并将其第一个元素与“simple”进行比较。


是的,这些主要的新特性都很好,但是C++程序员们所关心的那些很重要的问题呢?
  · 传统的“threads-and-locks”风格的系统级并行计算的类型安全得到了支持。和一个可以用于无锁(lock-free)编程的新的内存模型一起,它们将共同为C++程序员们编写更加高效,更具备可移植性的并行计算程序提供强有力的支持。
  · C++0x提供了一个更高抽象层次的并行计算模型。这个并行计算模型基于异步地执行多个任务,而这些任务之间又是通过所谓的消息缓冲(message buffer)进行通信的。
  · 一个新的正则表达式标准库组件
  · 哈希容器
  · 移动语义以及移动语义在标准库中的应用。特别地,现在我们可以以传值的方式从函数返回一个体积比较大的对象。例如:
  vector<int> make_vec(int n)
  {
      vector<int> res;
      for (int i=0; i<n; ++i) res[ii] = rand_int(0,100000);
      return res;
  }
  
  标准库中的vector有一个移动构造函数(move constructor),它可以接受一个右值并简单地直接将其转换为目标对象,而不是复制容器中的所有元素来完成对象的创建。这就表示函数的返回可以通过简单的少数几次赋值完成,而不再是通过更多的,比如一百万次,逐个复制元素来完成函数的返回。这样,我们无需再使用繁琐而危险的指针,引用,内存的申请和释放等。移动语义为我们传递大体积的对象提供了一个全新的完整的解决方案。特别地,它的实现也非常简单,并且使得对于数据的操作更加富有效率,比如两个矩阵的乘法操作:
  Matrix operator*(const Matrix&, const Matrix&);
  我可以一直继续下去,但是那将是一个长长的列表,但是这也将我们引向了更有趣的第二个问题:人们应该如何学习和使用C++0x中的这些新特性?我正在写第四版的《C++编程语言》(4th edition of The C++ Programming Language),但是那还有很多工作要做,那将会花费超过一年的时间。我想一定有其他的技术作者正在写或者正打算写关于C++0x的书,但是专家们或者C++的初学者想要找到比较好的书以及技术参考资料,恐怕还要等一段时间。(译注:我正在写一本全面覆盖C++0x新特性的C++参考书《我的第一本C++书》,即将由华中科技大学出版社出版,敬请期待)幸运的是,现在已经有一些关于C++0x的早期技术资料了,比如我的C++0x FAQ,它提供了很多简短的例子,以展示C++0x的核心特性,标准库的改建以及其它现在可以使用特性。但是,我们还需要更多的FAQ以及在线文档。我们还需要一些成系统地解释如何使用C++0x的新特性从而更好地支持C++的开发的资料。基于这样的考虑,我们需要的应该是一本书。
  当我在写程序的时候:使用C++的原则和实践经验,并假设我正在使用的编程语言是C++0x,但是,如果不使用C++0x的新特性那将是一件非常痛苦的事情。我可以十分肯定地预言,对于C++的培训者和学习者,C++0x将是上天的恩赐。C++0x对于对于一些好的编程技术和风格提供了大量的更有力的支持。比如,现在我们有一个普遍一致的初始化机制,现在我们都统一使用“{...}”操作符完成变量的初始化工作,并且不管我们在什么地方使用“{v}”初始化一个变量X,我们都会得到相同的结果。对于C++98中不一致的初始化形式,“=v”、“={v}”和“(v)”,这是一个非常大的改进。


  vector<double> v = { 1,2,3,4};    // a user-defined type
  double a[] = { 1,2,3,4};        // an aggregate
  int f(const vector<double>&);
  int x = f({1,2,3,4});
  auto p = new vector<double>{1,2,3,4};
  struct S { double a, b; };
  S s1{1,2};            // has no constructor
  complex<double> z { 1,2,};    // has constructor






在我们从C++0x的简化中获得好处之前,也许我们会经历这样一个黑暗的时期——很多人会通过列举C++0x的新的语法规则或者是孔乙己式地深究C++0x的语法细节来展示自己的“聪明才智”。实际上,这样做是有害的。
  我们不能指望人们仅仅通过阅读就能对C++0x编程有一个很好的理解。人们必须在开发实践中真正地使用这些新特性。幸运的是,C++0x的很多新特性已经在很多编译器(例如,GCC和Microsoft Visual C++)中实现了。C++0x不是象牙塔中的科学研究,而是真实地来到了我们身边。!
  Danny Kalev: 总体而言,你认为将右值引用添加到C++0x是值得的吗?除了性能的提升之外,一个典型的C++程序员还能从右值引用中获得什么其他的好处呢?比如更简洁的设计,更简单的算法等等?
  Bjarne Stroustrup: 我觉得将右值引用加入C++0x,不仅仅是值得,而是非常值得。移动语义可以作为一个长期存在的问题——如何从函数中返回一个体积较大的数据结构——的解决方案。对于这个问题,移动语义给了我们一个显而易见的,简单而高效的答案:直接将结果从函数中移动到目标位置;不需要复制结果;不需要在内存管理上玩什么技巧;不需要使用混乱的特殊用途的内存管理方案;不需要函数的调用者预先申请内存;不需要通过额外的参数进行值的传递;不需要任何形式的垃圾回收机制。我认为这是右值引用的两个应用中的最重要的一个。它将影响我们使用C++进行开发的每一个人,并且会让我们的生活变得更好。开玩笑地说,以前很多人都说“聪明的程序员才能使用C++”,现在,有了移动语义,不那么“聪明”的程序员也可以使用C++了。我们可以省掉我们的聪明了。
  值得注意的是,写一个有关右值引用移动的操作通常是一件非常简单的事情,它不像送火箭上天那么困难啦。
  class Matrix {


  double* elem; // 指向成员变量的指针


  int dim1, dim2;


  public:


  Matrix(Matrix&& a)


  :dim1(a.dim1), dim2(a.dim2), elem(a.elem) // 移动数据


  { a.dim1=0; a.dim2=0; a.elem=nullptr; } // 将原来的数据清空


  // ….


  };
  这就是整个移动构造函数的完整过程:移动数据并将原来的数据清空。有了它的帮助,我们甚至可以简单而高效地返回一个10000*10000的矩阵。
  当然,对于程序库的开发者而言这也将是非常重要的一天:在程序库中,有很多地方可以使用移动语义以简化程序库的实现,并且在更多的地方,转发(右值引用的另外一个重要应用)将有助于程序库的设计与实现。并且,移动语义和完美转发并不是只有专家才能掌握的高深技术,每一个C++程序员都可以使用它们来简化我们的程序,提高程序的性能。


当然,对于程序库的开发者而言这也将是非常重要的一天:在程序库中,有很多地方可以使用移动语义以简化程序库的实现,并且在更多的地方,转发(右值引用的另外一个重要应用)将有助于程序库的设计与实现。并且,移动语义和完美转发并不是只有专家才能掌握的高深技术,每一个C++程序员都可以使用它们来简化我们的程序,提高程序的性能。
  Danny Kalev: 在一些C++0x新特性,诸如右值引用,Lambda表达式,的设计中有很多困难。一些批评者也声称,C++太老了并且不够灵活。这些抱怨是否有一定的道理?会不会在将来的某一天,你决定不再扩展和改进C++,转而使用一种新的编程语言代替?
  Bjarne Stroustrup: 我更经常听到抱怨是C++太灵活并且太大。新的语言往往是比较简单的,因为它还没有形成一个庞大的社区。所有语言都会随着时间的流逝而增长。当然,修改一门大型的,有悠久历史并已经被广泛使用的编程语言要比推倒一切重来困难得多。但是,很多新语言都会夭折。并且,对于真实世界的应用来说,这些新语言显得太过简单了。向C++中添加新的内容是非常苦难的,对于一个新特性的建议者来说,要让这个新特性获得接受的过程通常也是漫长和痛苦的。但是,一旦这个新特性获得接受,它将会对很多人产生十分重大的影响。如果我不想影响整个世界,我完全可以通过填字游戏,写小说或者是设计一门好玩的编程语言来让自己的才智得到发挥。
  当然,我也曾经梦想过设计一门比C++更新的、更小的、更好的编程语言,但是,每当我看到这门新语言可以解决的问题,以及这门新语言可能产生的影响,我就觉得大多数通过一门新的编程语言可以解决的问题同样都可以通过改进C++及其标准库来获得解决。就对编程世界产生积极影响——至少对于我来说——而言,看起来比较繁琐的对C++的不断改进要比设计、实现和推广一门新的编程语言要好得多。
  Danny Kalev: 关于对Unicode的支持,C++0x提供了char_16和char_32,以及u16string和u32string来支持UTF16和UTF32编码的字符串。但是,它们在标准库中的输入输出流中并没有得到支持。例如,标准库中没有所谓的u16cout或u32cout。我想知道的是,我们该如何使用char16_t字符串并将它们输出?
  Bjarne Stroustrup: 显然,我们应该有支持unicode的输入输出流以及在标准库中的其他的扩展对unicode进行支持。标准委员会知道这样的需要,但是没人有足够的能力和时间来实现它。因此,不幸的是,这是C++中一个你不得不寻求第三方支持的地方。实际上有很多现有的程序库都可以很好地支持unicode,例如,可以用于构建网络应用以及互联网应用程序的Poco库(http://pocoproject.org/index.html)。另外,Microsoft Visual C++对Unicode也有很好的支持。
  不幸的是,我们并没有从标准库的层次上为unicode提供完整的支持,并且我们应该记住,大多数程序库并应该也不能够被包含在标准库中。我的C++页面上有很多关于程序库,程序库收集以及程序库列表的链接,大约估计有超过10000个C++程序库(包括商业的和开源的)。但是问题的关键是,你必须找到合适的程序库并评估它们。
  Danny Kalev: 最后,兔年已经到了,能不能和我们分享一下你的新年愿望是什么呢?
  Bjarne Stroustrup:
  · 让C++0x成为一个正式的ISO标准
  · 完成《C++编程语言》第四版的初稿
  · 和我的外孙共度更多的美好时光
  · 提出至少一个感兴趣的新的技术观点


QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 7楼 发表于: 2011-08-15
《C++0x漫谈》系列之:Concept
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)。所以我就不作重复劳动了:),我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。

Concept

好吧好吧,我承认我跳票了,上次说这次要写variadic templates的。但g9老大写了一篇精彩的散记,让我觉得concept应该先写,因为这实在是个有意思的特性,比variadic templates有意思多了。

我和Concept不得不说的事
事儿#1
看看下面这坨代码有什么问题:

std::list<int> li;
std::sort(li.begin(), li.end());

如果对人肉编译不在行的话,可以用你手头的编译器试一下。你会发现,你的编译器一碰到这简单而无辜的两行代码便会一反常态,跟个长舌妇似的吐出一大堆&#$@*^,令人牙酸的错误信息来。在使用C++模板库时这种编译错误井喷是家常便饭,动辄喷出令人应接不暇的4K字节的错误信息出来。你还以为不是编译器井喷,而是你自己RP井喷了,于是一脸无辜地跑去问模板达人,后者抬了抬眼皮,告诉你说“把list改成vector因为list的iterator不是random的而std::sort需要random的iterator”,你一边在脑子里给这句话分词加标点符号一边想弄明白他是怎么从一堆毛线似的字符里抽象出这么个结论的。

实际上,这个问题比你想像得严重,其根本问题在于降低工作效率,你得在你本不需要花工夫的地方(人肉解析编译错误)花工夫;这个问题比你想像得普遍,乃至于居然有人把“能够独立地解决所有的编译与链接问题”也列在了“有实际开发工作经验”要求里面;这个问题比你想像得影响恶劣,因为你可以想像可怜的新手在两行貌似无辜的代码面前哭丧脸的模样——C++编译器就这样把一个可怜的潜在C++用户给扼杀了。你也可以想像为什么有那么多人不喜欢C++模板——其实语法只是其一个非主要的方面。

实际上你请教的那个达人并没有什么火星抽象能力,只不过是吃过的桥比你走过的盐还多而已。而这,还预示着另一个问题,就是能人肉解析模板编译错误居然也成为了衡量C++达人与否的一个标准…不信你去各个坛子上转一转看看有多少帖子是询问关于编译错误的问题的,其中又有多少是关于模板编译错误的。

更小概率的是居然还存在一个专门解析STL相关错误信息的“STL错误解码器”——STLFilt。这玩意帮你把编译错误转换成人能识别的自然语言,不错是不错。可惜STLFilt有了,BoostFilt呢?ACEFilt呢?我自己写的模板库呢?…

其实,造成这个问题的直接原因是C++的类型系统的抽象层次太低。C++的静态强(也有人说C++的类型系统其实是弱类型系统,anyway)类型系统所处的抽象层面是在基本类型(int、double、char…)层面的。一方面,C++虽然拥有对自定义类型的上乘支持(比如,支持将自定义类型的接口装扮得跟内建类型几乎毫无二致——vector vs. build-in array),然而另一方面,C++的类型系统却对于像vector这样的抽象从语意上毫不知情。直接的后果就是,一个高层的类型错误往往以相差了十万八千里的底层类型错误表现出来,结果就是你得充当一次福尔摩斯,从底层错误一直往上回溯最终找到问题的发生点。譬如一开始给出的那个例子:std::sort(li.begin(), li.end());的错误,如果C++类型系统的抽象层能高一些的话(所谓抽象层次高,就是知道高层抽象概念(Concept)的存在,如“随机迭代器”这个概念),给出的错误无非就是:“list的迭代器不满足随机迭代器这个概念(concept)的要求(requirements)”。然而由于C++并不知道所谓concept的存在,所以问题到它眼里就变成了“找不到匹配的operator+…”一堆nonsense。

事儿#2
大二上学期的时候我们上一门计算方法的课程,期末考试要写一些矩阵算法。地球上的程序员大抵都知道矩阵算法不用Matlab算基本等于没事找抽,一大堆accidental complexities在那恭候着,一个index错误能让你debug到抓狂。当时我C++用得半斤八两,模板七窍也差不多通了六窍;为了到上机考试的时候节省点时间,就事先写了一个简单的矩阵库,封装了一些基本的操作和像高斯消元这种基本算法。

那个时候你能指望我知道TDD?还是XP?或者STLLint?于是呢?写了一个简单的程序,简单使用了一下写好的库,发现编译通过后就兴冲冲地告诉哥们说:大家不用怕,有我这Matrix库罩着,写算法跟写伪码差不到哪去!

两天后上机考试,程序不同了,等于测试用例不同了,结果原来没有出现的编译错误一下统统跑出来了。原来为什么不出现?一个原因是原来有些成员函数就没用到,C++说,在一个模板类里面,没用到的成员函数是不予编译的。那不予编译就代表不予纠错吗?不予类型检查吗?令人悲伤的是,的确如此。或者把置信度提高一点说,几乎如此。为什么?看看下面的代码:

template<typename T>
void f(T& t)
{
t.m();
}

你说编译器看着这个函数,它怎么做类型检查?它怎么知道t上面有没有成员函数m?它连t的类型都不知道。“很久很久以前,模板就是这样破坏模块式错误检查的…”

实际上,C++98那会,为了能够尽早尽量检查模板代码中的隐患,以响应“防范胜于救灾,隐患重于明火”的号召,C++甚至将模板上下文中的代码中的名字生生分成了两类,一类叫dependent names,一类叫non-dependent names。举个例子,上面那段代码中的m成员函数就是dependent的,因为它的隐含this参数t的类型是dependent的;对于dependent name,不作类型检查——原因刚才讲过,因为类型信息根本就没有。剩下的就是non-dependent names了,比如:

void g(double); // #1

template<typename T>
void f()
{
g(1);
}

void g(int); // #2

int main()
{
f<int>();
}

这里f里面调用的g绑定到哪呢?答案是#1。因为g是个non-dependent name(虽然它位于模板函数(上下文)里面)。而对于non-dependent name,还是赶紧进行类型检查和名字绑定吧,有错误的话也能早点暴露出来,于是g便在它的使用点“g(1)”处被查找绑定了——尽管#2处的g(int)是一个更好的匹配,但在g(1)处只有g(double)是可见的,所以g(double)被编译器看中了,可怜的g(int)只能感叹“既生g(int),何生g(double)…”。

这,便是臭名昭著的腰斩…sorry…是二段式名字查找,C++著名的复杂性来源之一。说它臭名昭著还有一个原因——在众多编译器支持良莠不齐的C++复杂特性中,它基本可以说是位居第一(第二估计要留给友元声明了),VC挣扎到8.0还是没有实现二段式名字查找,而是把所有的工作留到模板实例化点上进行,结果就是上面的例子中会选中#2。

D&E中对此亦有详细介绍。

实际上,这个二段式名字查找的种种问题正从一个侧面证明了早期类型检查是何等重要,动态语言的老大们在Ruby翻出来的旧瓶新酒Duck Typing上吵翻了天其实说的也是这个问题(sorry,要加上“之一”)。

事儿#3
在一个无聊的午后,我在敲打一坨代码,这是一个算法,算法要用到一个容器,算法是用模板来实现的:

template<typename ContainerT>
void XXXAlgo(ContainerT cont)
{
… cont.

在我敲打出“cont”加点号“.”之后,我习惯性地心理期待着“智能”的IDE能够告诉我cont上面有哪些成员函数,正如我们每次敲打出“std::cout.”之后一样。习惯成自然,你能说我不对么?难道你金山糍粑用久了不也一样在读影印版纸书遇到不认识单词的时候想着把手指头伸过去指着那个单词等着跳出个词条窗口来?难道只是我?咳咳…

问题是,我知道XXXAlgo的那个模板参数ContainerT是应当符合STL的Container概念(concept)的,我当然希望编译器也能知道,从而根据Container概念所规定它必须具有的成员函数来给我一个成员函数列表提示(begin,end,size…),难道这样的要求很过分吗?它没有道理很过分啊,觉得它很过分我会说的啊,不可能它明明不过分我偏要说它很过分,他很过分我偏要说它不过分啊…你觉得这要求过分你就说嘛…咳…乱敲键盘是不好滴,键帽掉下来砸到花花草草也不好啊…你看,“.”键又给你磨平了…

一方面,程序员一脸无辜地认为IDE应该能够看到代码里面的ContainerT暗示着这是一个符合STL的Container概念的类型。而另一方面IDE厂商却也是理直气壮:写个ContainerT就了不起啊,万一遇到个C过来的,写成ContT我怎么办?写成CntnrT哪?是不是要我实现一个spell checker?再说你觉得ContainerT是对应STL的Container概念的,别人还用这个单词来对应线程池呢怎么办捏?什么?他不知道“poor”怎么写管我啥事嘞?我身为一个IDE,根据既有的信息,作出这样的假设,既合情,也合理…

事儿#4(此事纯虚虚构,如有巧合,算你运气背)
一天,PM跑过来告诉你说:“嘿,猜怎么着,你写的那坨模板代码,隔壁部门人用了说很不错,希望你能把代码和文档完善一下,做成一个内部使用的库,给大家用,如何?”你心头一阵花枝乱颤:“靠!来部门这么久了,C++手段终于可以展露一下了。”于是废寝忘食地按照STL文档标准,遵照C++先贤们的教诲,写了一个漂漂亮亮的文档出来。里面Concept井井有条,Requirements一丝不苟…

动态语言的老大们常挂在嘴边的话是什么?——需求总是在变的。又一天,你发现某个Concept需要revise了,比如原来的代码是这样的:

template<typename XXX>
void f(XXX a)
{
  …
a.m1();
}

本来XXX所属的那个Concept只要求有m1成员函数。后来因需求变更,XXX上需要一个新的成员函数m2。于是你的代码变成了:

template<typename XXX>
void f(XXX a)
{
  …
a.m1();

a.m2();
}

但仅改代码是不行的,文档里面关于XXX所属的那个Concept的描述也要同步修改…可惜天色已晚,良宵苦短,你准备睡一觉明天再说…结果第二天一早你就被boss叫去商量一个新的项目(因为你最近表现不错),于是你把这事给忘了。于是跟代码不一致的文档就留在那里了…

这种文档和代码不一致的情况太常见了,根本原因是因为代码和文档是物理上分离的,代码不能说谎,因为要运行,但文档呢?什么东西能验证文档精确反映了代码呢?除了往往忽视文档的程序员们之外没有其他人。这个问题是如此广泛和严重以至于程序员们干脆就近乎鸵鸟式地倡导“代码就是文档”了,这句话与其说是一个陈述句,不如说是一个美好的愿景(远景?)。

好吧,好吧,你记性好,这点小事你不会忘掉,第二天一早你就把文档给改了,你真是劳模。可惜过一天,需求居然又改变了(你心说是哪个家伙负责客户需求分析的?!),这下你需要修改Concept继承体系了…

你看,可能造成文档和代码脱节的因素太多了,一般一段时间以后,能说得上话的也就剩代码,文档只能拿来看看“系统应该是什么样子的”,只有代码才陈述了“系统实际是什么样子的”。

然而,如果文档就在代码当中呢?不,我不是说注释,你又不是不知道要写出合格的注释比写出合格的小说还要难。我是说,代码就是文档文档就是代码…

此外,把Concept约束写在代码里面还有一个好处就是能够使得被调用函数和调用方之间的契约很明显,Concept的作用就像门神,告诉每一个来调用该函数的人:“你要进去的话必须满足以下条件…”。Ruby的Duck Typing被诟病的原因之一就是它的Concept在代码里面是隐式的,取决于对象上的哪些方法被使用到了。

事儿#5
重构重不重要?Martin Fowler叔叔笑了…

原来我抽屉里有这么一段代码:

template<typename XXXConcept>
void foo(XXXConcept t)
{

t.m1(); // #1

}

template<typename XXXConcept>
void bar(XXXConcept t)
{

t.m1(); // #2

}

现在我想对代码作一种最简单的重构——改名。m1这个名字不好听,我想改成mem。于是我指望编译器能替我完成这个简单的任务,我把鼠标指到#1处,在m1上右击,然后重命名m1为mem。同时很显然我期望“智能”的编译器能够帮我把#2处也改过来,因为它们用的是同一个concept上的成员函数。

但编译器不干,原因见事儿#3。或者见这篇blog,后者举了一个类似的例子——如果我们重命名实现了那个XXXConcept的类上的m1方法,那么#1和#2处的调用能被自动重命名吗?Ruby Refactoring Browser的作者笑了…

事儿#6
很久很久以前…我写了一个容器类。这个容器类里面该有的功能都有了…唯一的问题是,当时我还不知道STL(准确地说是就算知道也没用),结果呢?这个各方面功能都完备的容器类的使用界面(接口)并不符合STL容器的规范。比如我把begin()叫做start(),把end()叫做…还是叫做end()(不然总不能叫finish()吧?)。我还把empty()叫做isEmpty()了…而另一方面我的empty()实际却做的是clear()的工作…

后来,我又写了一个算法,这个算法是针对STL容器的,你问我干嘛不针对迭代器编程?很简单,因为我要用到empty、front/back、clear等成员函数。基于迭代器编写也…不是不行,就是得再费一袋烟…此外还有两个问题,一是效率,而是影响算法使用…呃…说到效率…

现在,我想让我的这个算法也能操纵我原来那个古董容器(我不是指我家那个慈禧尿壶),但因为那个古董容器的接口跟我的算法要用到的接口不一致:

class MyCont { … bool isEmpty(); … };

template<typename Cont>
void f(Cont cont){ … cont.empty(); … }

怎么办?修改MyCont的实现?可以,因为这个MyCont是我写的,后者意味着两点:一,我有权修改它。二,我写的库没其他人用。可是如果MyCont是位于另一个库当中的呢?如果有一堆依赖于它的既有代码呢?

或者,写个wrap类?还是太麻烦了,况且wrap手法也不是没有自己的问题。我们只不过想适配一下接口而已。

其实,我们只想对编译器说一句:MyCont的isEmpty其实就是empty,您行行好就放行吧…

事儿#7
函数重载重不重要?废话。多态重不重要?还是废话。

那SFINAE重不重要?C++ Templates的geeks们都笑了…

在C++里面,SFINAE技术已经成为GP的奠基技术之一(老大当然是sizeof技术)。boost里面为此专门引入了一个库,叫boost::enable_if。该库,如boost::mpl一样,被boost里面的众多子库依赖。比如boost::function库就用到了该技术。

简而言之,SFINAE技术允许你根据类型的编译期信息实现多态行为:

template <class T>
typename enable_if<boost::is_arithmetic<T>, T>::type
foo(T t) { return t; }

如果T是算术类型,那么这个foo函数模板就能够实例化,否则就不会。

另一项相关的模板技术是Tag Dispatch。STL中大量运用这种手法:

template<typename InputIter>
typename iterator_traits<InputIter>::difference_type
distance(InputIter it1, InputIter it2)
{
  return distance(it1, it2,
typename iterator_traits<InputIter>::iterator_category());
}

如果typename iterator_traits<InputIter>::iterator_category是random_access_iterator_tag的话就会跳转到:

… distance(InputIter it1, InputIter it2 , random_access_iterator_tag)
{ … }

如果是bidirectional_iterator_tag的话就会跳到:

… distance(InputIter it1, InputIter it2 , bidirectional_iterator_tag)
{ … }

然而,这些与其说是技术(techniques),不如说是技巧(tricks)。它们的存在增加了C++中GP的accidental complexity。它们,正如大多数的C++模板技巧一样,本不在C++设计的考虑之内,而是后来被人们发现出来的。当然你可以说C++是唯一一门语言之父本人需要别人教他怎么用的语言,这的确很奇妙,然而当程序员抓耳挠腮地对付这些技巧的时候,恐怕更多的是恼火。比如这个:

template <typename _Iterator>
struct iterator_traits {
  typedef typename _Iterator::difference_type difference_type;
};

template <typename _InputIterator>
inline typename iterator_traits<_InputIterator>::difference_type
distance(_InputIterator, _InputIterator);

double distance(const int&, const int&);

void f() {
  int i = 0;
  int j = 0;
  double d = distance(i, j);
}

符合SFINAE的条件吗?不符合吗?符合吗?…

说到底,这些细节本不该由程序员来操心。我们想要的是一个简洁明了地表达我们想法的工具。

事儿#8
如果一个生物走起路来像个火星人,
说起话来像个火星人,
回起贴来像个火星人,
那他肯定就是火星人。
——火星人判别最高纲领

Ruby的串红使火星人类型系统焕发出了第二春。

Rubyers的口号是,我不关心你是不是真的是火星人,看你丫的回帖像刚从火星回来的,你一定就是火星人!

Sorry,用严肃一点的话来说,就是“不关心一个对象的具体类型,而只关心一个对象的行为”。用镐头书上的例子就是:

class Customer
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def append_name_to_file(file)
file << @first_name << " " << @last_name
end
end

file << @first_name,这里file并不一定要是真正的文件,而只要是一个支持“<<”操作的对象即可(想起C++的流插入符了吗?)。所以要测试这个Customer类的append_name_to_file,也就不一定要真正创建一个文件出来传给它当参数,只要传一个支持<<操作的对象给它就可以了——比如一个String对象。用String对象的好处就是检查被写入到这个String里面的东西很容易,而用File对象的话还得开文件关文件的,麻烦。

事实证明Ruby的火星人类型系统是非常灵活的。镐头书上还举了另一个实际的例子:有这么一坨代码,它遇到数据量大的时候就变得奇慢,项目期限在即,当花了一点时间检查问题所在之后,发现问题在于代码中创建了许多的String临时对象,而速度问题则是因为GC运行起来了,之所以有这么多String临时对象是因为一个循环里面不断往一个String对象上面Append新子串,导致原来的串对象被丢弃,一地鸡毛。

结果还是火星人类型系统“to the rescue”。通过仅改变一两行非关键代码,该项目得救了。作出的改变就是把那个String对象换成一个Array,这样每次往上面Append新串的时候都会把这个新串当作一个新的元素挂到这个Array对象上,活像一串串腊肉;由于没有旧串被丢弃,因此也就不会出现遍地垃圾的情况。

好吧,我承认我在练习中学语文老师教的欲抑先扬手法。不过Ruby Fans大可不必激动,因为两个原因:一,C++也有同样的问题,所有的模板代码用的本质上也都是火星人类型系统(C++98只支持完全unconstrained templates)——管你实际上是不是迭代器,只要你有++、--、*、->等操作就行。二,火星人…类型系统的危险是理论上的,实际上谁也没有案例证明它导致了什么灾难。比如在Cedric的blog上这篇“The Perils of Duck Typing”后面就有人跟贴说一个JarFile上有一个explode(解压)和一个NuclearBomb上有一个explode(爆炸),于是你的算法把一个核弹给“解压”(爆炸)了。从这个例子的极端也不难看出其实这种危险的可能性并不大。有一次我在新闻组上发帖,扯到这个问题上,也有人举了一个例子,说手指有一个方法叫“插”(Plug),而插头也有一个方法叫“插”(Plug),于是不管三七二十一的算法就面临把一根手指(谁的谁倒霉)插到插座中去的危险。

这些例子说到底都有点飘逸,不切实际。具体的例子你问我我也没有,或许C++里面倒是可以捏造出一个“比较”实际一点的来:

template<typename StreamT>
void f(StreamT& stream) { stream << 1; }

int i;

f(i);

这段代码编译器也乐呵呵地编译了,因为整型是支持<<(位移)操作的。这里的错误很显然,但若是藏在成千上万代码当中,因为一个打字错误而漏进去的话,也许就不那么显然了。

从本质上说,火星人类型系统是将语法结构的同一性视为语意层面的同一性(即所谓的Structural Conformance);这才是它的根本问题。而另一方面,传统的接口继承(即所谓的Nominal Subtyping)则更严格:当你继承自一个接口的时候,你明确而清醒地知道你是要实现该接口所表达的抽象(语意),你不会“一不小心”(accidentally)实现了一个接口的,你必须写上“implements …(Java)/ public … (C++)”几个大字…母才行。

Concept to the rescue
话说到这份上如果你还不知道我要说什么…咳…那我就继续说吧…

以上八大问题一直都是GP中被广为争论的问题,其中duck typing(#8)在动态语言社群争论得比在C++里面还要激烈得多;同时它们也都是由来已久的问题,有的甚至久远到Bjarne在D&E中就已经遇见到了,只是当时C++标准化的进度太紧来不及解决而已,这一晃就是10年…

没错,它们全部都可以用Concept来漂亮地解决。或者换个说法,Concept的出现就是为了解决以上这些问题的——

#1(编译错误问题)——有了Concept,C++的类型系统抽象层次便提高了一个级别,在遇到编译错误的时候便能说“XXX不满足XXXConcept”这样的话了。

#2(模块式类型检查问题)——有了Concept,原本所谓的unconstrained templates便可以做成constrained。比如STL的for_each算法就变成了这样:

template<InputIter Iter, Callable1 Fun>
Fun for_each(Iter first, Iter last, Fun func)
{
  for(;first != last; ++first) func(*first);
  return func;
}

其中InputIterator和Callable1都是Concepts:

auto concept Callable1<typename F, typename X> {
  typename result_type;
  result_type operator()(F&, X);
};

concept InputIterator<typename X> : EqualityComparable<X>, …
{


  pointer operator->(X);
  X& operator++(X&);
  postincrement_result operator++(X&, int);
  reference operator*(X);
}

有了这些Concept,编译器在对for_each作类型检查的时候便能够往InputIterator/Callable1里面进行名字查找:“first != last”可不可行?只要看first的类型支不支持“!=”,那first的类型支不支持“!=”呢?因为first的类型Iter是满足concept InputIterator的,那就只要看InputIterator里面有没有“!=”就行了,有吗?没有?哦,不好意思,忘记说了,InputIterator是继承自EqualityComparable的,后者里面定义了“!=”。

auto concept EqualityComparable<typename T, typename U = T> {
bool operator==(T a, U b);
bool operator!=(T a, U b) { return !(a == b); }
}

同样的,“++first”这个表达式可行吗?只要看first的类型支不支持“++”就可以了,后者只要看InputIterator这个concept支不支持“++”,答案是支持。

#3(IDE智能提示问题)——编译器既然知道了concept的存在,当你敲下iter,ctrl+空格的时候编译器便能够通过解析InputIterator这个concept的定义来告诉你iter对象支持哪些操作了。

#4(文档代码分离问题)——瞧一瞧for_each的声明,原来(C++98)是:

template<typename InputIterator … >
void for_each(InputIterator iter …);

现在(C++09)是

concept InputIterator
{

}

template<InputIterator Iter … >
void for_each(Iter …);

区别在什么地方?原来的代码中没有concept InputIterator这样的声明,你看着InputIterator这么个单词,得去STL的文档里面查才知道它到底有那些requirements。有了concept之后呢?只要翻开InputIterator这个concept的定义就看到了,后者将位于C++09的<iterator>头文件中。

#5(重构问题)——重构?当然!有了concept,要重构的时候只要修改concept定义,所有使用了该concept内的函数的地方都可以容器地作出改变。

#6(接口适配问题)——实际上前面“事儿#6”里面提到的古董容器+非古董算法的例子虽然能说明问题,但总是不够巧妙。还不如直接抄Concept六君子在OOPSLA ‘06上的牛paper中的例子,Douglas在paper里面举了一个图论库的例子:有一个矩阵算法,但另外还有一个图(Graph),就算没吃过图总见过图走路吧——图是可以用矩阵来表示的,所以只要用concept_map把图类的接口适配一下就可以拿那个现成的矩阵算法来操纵了,如下:

template<Graph G>
concept_map Matrix<G>
{
  typedef int value_type;
  int rows(const G& g) { return num_vertices(g); }
  int columns(const G& g) { return num_vertices(g); }
  double operator()(const G& g, int i, int j)
  {
    if (edge_e = find_edge(ith_vertex(i, g), ith_vertex(j, g), g))
      return 1;
    else return 0;
  }
};

#7(SFINAE、traits、tag dispatching等等)——有了concept,前文的代码就可以这么写:

template<Arithmetic T>
T foo(…) { … }

只有对于符合Arithmetic这个concept的类型T,该函数才“存在”。

concept还给函数重载带来了更强大的能力,比如:

concept Initializable<typename T>
{
  void initialize();
}

template<Initializable T>
void foo(…) { … }

template<typename T>
void foo(…) { … }

为什么说它更强大?不信你在C++98下实现看看。

#8(火星人…类型系统问题)——火星人的危险性前文已经阐述了。其危险在于将语法结构同一性视为语意同一性。用传统的接口继承就没有这个问题,因为当你继承自一个接口的时候,你明确知道你在干嘛——实现这个接口的语意。因此,在C++09的concept中,缺省的concept是“非auto”的,也就是说:

concept Drawable<typename T>
{
  void T::draw() const;
}

class MyClass
{
  void draw() const;
};

这种情况下MyClass是不会自动满足Drawable这个concept的(尽管它的确实现了一个一模一样的draw()函数),这是为了避免无意间实现了一个不该实现的concept。要想让MyClass实现Drawable这个concept,必须显式地说明这一点:

concept_map Drawable<MyClass> { }

是不是看上去很像模板特化?实际上concept的内部编译器实现正是利用既有的一套模板特化系统来进行的。

但是如果每个concept都要靠concept_map来支持的话太麻烦,有些基本的concept比如EqualityComparable——只要一个类型重载了operator==,那么就肯定是EqualityComparable的。这个论断几乎肯定是安全的,因为没有谁会不知道operator==的语意吧?所以,EqualityComparable是auto的:

auto concept EqualityComparable<typename T, typename U = T> {
bool operator==(T a, U b);
bool operator!=(T a, U b) { return !(a == b); }
}

这样一来如果你的类实现了operator ==,你不需要将它concept_map到EqualityComparable,就能自动(auto)实现EqualityComparable;一句话,回到原始的“结构一致性”上面。

关于auto的另一个作用,我想到了一个绝妙的介绍,但这里空白太小写不下了,请听下回分解:-)

延伸阅读
[1] Concepts: Linguistic Support for Generic Programming in C++
此篇是Concepts的权威饲养指南,高屋建瓴巨细靡遗地介绍了Concept的方方面面。
[2] An Extended Comparative Study of Language Supports for Generic Programming
此篇对各种语言对GP的支持做了极其详尽的survey,其中也提到了concept的一些东西,很有价值的一篇paper。
[3] Concept checking – A more abstract complement to type checking
当年,C++的老豆率先发难,写了这篇最早的concept paper,其间对三大实现策略作了高屋建瓴的比较,对掌握concept的本质有非常好的帮助。
[4] Concepts
一番刀光剑影你来我往之后,user-pattern派(由Bjarne本人发起)和function signature派(由Douglas带领)终于联合起来;这是第一篇署名Bjarne Stroustrup & Douglas Gregor的Concept Proposal。Function Signature的做法被正式确定下来(主要原因之一是它提供了#6(类型适配)这个大大的好处)。
[5] Proposed Wording for Concepts(rev#1)
这个就不用说了,截止到最近的concepts标准提案。
[6] Concepts for the C++0x Standard Library Utilities(rev#2)
这个自然也不必说了,C++0x标准库里面的一些基本的concepts定义。
[7] http://www.generic-programming.com
ConceptGCC的官方站,含GCC实现的下载,以及历届concepts相关paper。
[8] Yet Another Haskell Tutorial
其实concept在Haskell里面早就实现了,不过名字不叫concept,而叫type class。看一看haskell的实现对理解C++09 Concepts肯定也是有帮助的。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 8楼 发表于: 2011-08-15
《C++0x漫谈》系列之:瘦身前后
瘦身前后——兼谈语言进化


前一阵子写了一篇文章,提到语言进化的职责之一,就是去除语言中的tricks(职责之二是去除非本质复杂性)。

常看我blog的朋友肯定记得我曾写过的boost源码剖析系列。本来这个系列是打算成书的,但随着对C++的认识发生了一些转变,对语言级技术的热衷逐渐消退,再回过头来看boost库中的一些组件,发现原本觉得很有写的必要的东西顿时消失了。Scott Meyers的主页上也列有一个写Boost Under The Hood的计划,一直也不见成文,兴许也有类似的原因。

一门语言应该是“Make simple things simple, make complex things possible”的。当我们用语言来表达思想的时候,这门语言应该能够提供这样的能力:即让我们能够最直接地表达我们的意思,多一分则太多,少一分则太少,好比古人形容美女:增一分则太肥,减一分则太瘦。

这个问题上,有一个我认为是广泛的误解,就是“KISS便意味着要精简语言,并避免在编码中使用‘高阶’语言特性”。对此有一句话我觉得说得好:你不能通过从一门语言中去掉东西来增加表达力。高阶特性是一面利刃,用得不好固然伤了自己,但这并不表明就没有用。任何东西都是在它真正适用的地方适用,霸王硬上弓的话弓断弦崩反而伤及自身。所以,仅仅因为高阶特性容易误用(而且高阶特性的确也容易吸引人去用且容易误用,不过这是另一个问题),就断然在任何地方都不用并宣称这样才是KISS的话,便因噎废食了。举个例子,高阶函数是有用的,如果在真正需要高阶函数的地方不用高阶函数,那不是KISS,只能让解决方案(或者更确切地说,workaround)更复杂。lambda函数是有用的,但如果在真正需要lambda的地方不使用lambda,也只能导致更复杂更不直观的workarounds。OOP是有用的,但如果你的程序本来就只是简单的“数据+操作”你偏要硬上OOP的话,不仅多了编码时间,而且还降低程序的可见度和可维护性,后者就意味着项目的money。拿C++来说,这是一个广为诟病的问题。C++的偏向底层的应用领域决定了有不少地方使用C++其实就是“数据+操作”,然而很多人却因为用的是C++编译器,便忍不住去使用高级特性,结果把本来简单的事情复杂化——我自己就有不少次这样的经历:用了一大堆类之后,做完了回过头来再看,这些类都干嘛来着?需要吗?最关键的就是要清楚自己做的是什么事情,以及什么工具才是对你所做的事情最适合的。

说到这里不妨顺便说说另一个误解:“如果我反正用不着C++里面的高级特性,那还不如用C罢了”,鉴于C/C++的应用领域,的确有不少地方是可以用C++的C部分完成得很好的,所以这个误解被传播得还是蛮广泛的。这里的一个微妙的忽视在于:用C的话,你就用不到许多很好的C++库了。用C++的话,你完全可以在你自己的编码中不使用高阶特性(说实话,这需要清醒的头脑和丰富的经验,以及克制能力),但你还是可以利用众多的C++库来简化你的工作的:如果一个transform明明可以搞定的你偏要写一个for出来难道能叫KISS?如果一个vector就能避免绝大多数内存管理漏洞和简化内存管理工作你偏偏要手动malloc/free那能叫KISS(我见过不少用C++编码却到处都是malloc/free的)?如果最直接的方式是gc你偏偏要绕一大堆弯子才能保证正确释放那也不叫KISS(等C++09吧)。如果一个for_each(readdir_sequence(".", readdir_sequence::files), ::remove);能搞定的你偏要写:

// in C
DIR*  dir = opendir(".");
if(NULL != dir)
{
  struct dirent*  de;
  for(; NULL != (de = readdir(dir)); )
  {
    struct stat st;
    if( 0 == stat(de->d_name, &st) &&
        S_IFREG == (st.st_mode & S_IFMT))
    {
      remove(de->d_name);
    }
  }
  closedir(dir);
}

那能叫KISS?

总之还是那句话:明确知道你想要表达的是什么并用最简洁(在不损害容易理解性的前提下)的方式去表达它。但我认为,最KISS不代表最原始。

进化——两个例子
先举一个平易近人的例子(Walter Bright——D语言发明者——曾在他的一个presentation中使用这个例子),如果我们想要遍历一个数组,在C里面我们是这么做(或者用指针,不过指针有指针自己的问题):

int arr[10];
… // initialize arr
for(int i = 0; i < 10; ++i)
{
int value = arr[ii];[ii]

printf
}

这个貌似简单的循环其实有几个主要的问题:

1. 下标索引不应该是int,而应该是size_t,int未必能足够存放一个数组的下标。
2. value的类型依赖于arr内元素的类型,违反DRY,如果arr的类型改变为long或unsigned,就可能发生截断。
3. 这种for只能对数组工作,如果是另一个自定义容器就不行了。

在现代C++里面,则是这么做:

for(std::vector<int>::iterator
iter = v.begin();
iter != v.end();
++iter) {
  …
}

其实最大的问题就是一天三遍的写,麻烦。for循环的这个问题上篇讲auto的时候也提到。

Walter Bright然后就把D里面支持的foreach拿出来对比(当然,支持foreach的语言太多了,这也说明了这个结构的高效性)。

foreach(i; v) {

}

不多不少,刚好表达了意思:对v中的每个元素i做某某事情。

这个例子有人说太Na?ve了,其实我也赞成,的确,每天不知道有多少程序员写下一个个的循环结构,究竟有多少出了上面提到的三个问题呢?最大的问题恐怕还是数组越界。此外大家也都亲身体验过违反DRY原则的后果:改了一处地方的类型,编译,发现到处都是类型错误,结果一通“查找——替换”是免不了的了,谁说程序员的时间是宝贵的来着?

既然这个例子太Na?ve,那就说一个不那么Na?ve的。Java为什么要加入closure?以C++STL为例,如果我们要:

transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), _1 + _2);

也就是说将v1和v2里面的元素对应相加然后放到v3当中去。这里用了boost.lambda,但大家都知道boost.lambda又是一个经典的鸡肋。_1 + _2还算凑活,一旦表达式复杂了,或者其中牵涉到对其它函数的调用了,简直就是一场噩梦,比如说我们想把v1和v2中相应元素这样相加:f(_1) + f(_2),其中f是一个函数或仿函数,可以做加权或者其它处理,那么我们可以像下面这样写吗:

transform(…, f(_1) + f(_2));

答案是不行,你得这样写:

transform(…,
boost::bind(std::plus<int>(), boost::bind(f, _1), boost::bind(f, _1))
);

Lisper们笑了,Haskeller们笑了,就连Javaer们都笑了。It’s not even funny! 这显然违反了“simple things should be simple”原则。

如果不想卷入C++ functional的噩梦的话,你也可以这么写:

struct Op
{
int operator()(int a1, int a2) { return f(a1) + f(a2); }
};

transform(…, Op());

稍微好一点,但这种做法也有很严重的问题。

为什么Java加入closure,其实还是一个语法问题。从严格意义上,Java的anonymous class已经可以实现出一样的功能了,正如C++的functor一样。然而,代码是给人看的,语言是给人用来写代码的,代码的主要代价在维护,维护则需要阅读、理解。写代码的人不希望多花笔墨来写那些自己本不关心的东西,读代码的人也希望“所读即所表”,不想看到代码里面有什么弯子,最好是自然语言自然抽象才好呢。

所以,尽管closure是一颗语法糖,但却是一颗很甜很甜的糖,因为有了closure你就可以写:

transform(…, <>(a1, a2){ f(a1) + f(a2) });

Simple things should be simple!

此外,closure最强大的好处还是在于对局部变量的方便的引用,设想我们想要创建的表达式是:

int weight1 = 0.3, weight2 = 0.6;
transform(…, f(_1)*weight1 + f(_2)*weight2);

当然,上面的语句是非法的,不过使用closure便可以写成:

int weight1 = 0.3, weight2 = 0.6;
transform(…, <&>(_1, _2){ f(_1)*weight1 + f(_2)*weight2 } );

用functor class来实现同样的功能则要麻烦许多,一旦麻烦,就会error-prone,一旦error-prone,就会消耗人力,而人力,就是金钱。

C++09也有希望加入lambda,不过这是另一个话题,下回再说。

The Real Deal——variadic templates
C++的callback类,google一下,没有一打也有半打。其中尤数boost.function实现得最为灵活周到。然而,就在其灵活周到的接口下面,却是让人不忍卒读的实现;03年的时候我写的第一篇boost源码剖析就是boost.function的,当时还觉得能看懂那样的代码牛得不行...话说回来,那篇文章主要剖析了两个方面,一个是它对不同参数的函数类型是如何处理的,第二个是一个type-erase设施。其中第一个方面就占去了大部分的篇幅。

简而言之,要实现一个泛型的callback类,就必须实现以下最常见的应用场景:

function<int(int, int)> caller = f;
int r = caller(1, 2); // call f

为此function类模板里面肯定要有一个operator(),然而,接下来,如何定义这个operator()就成了问题:

template<Signature>
class function
{
??? operator()(???);
};

???处填什么?返回值处的???可以解决,用一个traits:typename result_type<Signature>::type,但参数列表处的???呢?

boost采用的办法也是C++98唯一的办法,就是为不同参数个数的Signature进行特化:

template<typename R, typename T1>
class function<R(T1)>
{
R operator()(T1 a1);
};

template<typename R, typename T1, typename T2>
class function<R(T1, T2)>
{
R operator()(T1 a1, T2 a2);
};

template<typename R, typename T1, typename T2, typename T3>
class function<R(T1, T2, T3)>
{
R operator()(T1 a1, T2 a2, T3 a3);
};

… // 再写下去页宽不够了,打住…

如此一共N(N由一个宏控制)个版本。

这种做法有两个问题:一,函数的参数个数始终还是受限的,你作出N个特化版本,那么对N+1个参数的函数就没辙了。boost::tuple也是这个问题。二,代码重复。每个特化版本里面除了参数个数不同之外基本其它都是相同的;boost解决这个问题的办法是利用宏,宏本身的一大堆问题就不说了,你只要打开boost.function的主体实现代码就知道有多糟糕了,近一千行代码,其中涉及元编程和宏技巧无数,可读性可以说基本为0。好在这是个标准库(boost.function将加入tr1)不用你维护,如果是你自己写了用的库,恐怕除了你谁也别想动了。所以第二个问题其实就是可读性可维护性问题,用Matthew Wilson的说法就是可发现性和透明性的问题,这是一个很严重的问题,许多C++现代库因为这个问题而遭到诟病。

现在,让我们来看一看加入了variadic templates之后的C++09实现:

template<typename R, typename... Args>
struct invoker_base {
  virtual R invoke(Args...) = 0;
  virtual ~invoker_base() { }
};

template<typename F, typename R, typename... Args>
struct functor_invoker : public invoker_base<R, Args...>
{
  explicit functor_invoker(F f) : f(f) { }
  R invoke(Args... args) { return f(args...); }
private:
  F f;
};

template<typename Signature>
class function;

template<typename R, typename... Args>
class function<R (Args...)>
{
public:
  template<typename F>
  function(F f) : invoker(0)
  {
    invoker = new functor_invoker<F, R, Args...>(f);
  }

  R operator()(Args... args) const
  {
    return invoker->invoke(args...);
  }

private:
  invoker_base<R, Args...>* invoker;
};

整个核心实现就这些!一共才36行!加上析构函数拷贝构造函数等边角料一共也就70行!更重要的是,整个代码清晰无比,所有涉及到可变数目个模板参数的地方都由variadic templates代替。“Args…”恰如其分的表达了我们想要表达的意思——多个参数(数目不管)。与C++98的boost.function实现真是天壤之别!

这里function_invoker是用的type-erase手法,具体可参见我以前写的boost.any源码剖析,或上篇讲auto的,或《C++ Template Metaprogramming》(内有元编程慎入!)。type-erase手法是像C++这样的弱RTTI支持的语言中少数真正实用的手法,某种程度上设计模式里面的adapter模式也是type-erase的一个变种。

如果还觉得不够的话,可以参考variadic-templates的主页,上面的variadic templates proposal中带了三个tr1实现,分别是tuple,bind,function,当然,variadic-templates的好处远远不仅仅止于这三个实现,从本质上它提供了一种真正直接的表达意图的工具,完全避开了像下面这种horrible的workaround:

template<class T1>
cons(T1& t1, const null_type&, const null_type&, const null_type&,
     const null_type&, const null_type&, const null_type&,
     const null_type&, const null_type&, const null_type&)
: head (t1) {}

更多的源代码见这里。tuple的C++98实现,代码近千行。利用variadic-templates实现,代码仅百行。

和这种更horrible的workaround:

template<class R, class F, class A1, class A2, class A3, class A4, class A5, class A6>
    _bi::bind_t<R, F, typename _bi::list_av_6<A1, A2, A3, A4, A5, A6>::type>
    BOOST_BIND(boost::type<R>, F f, A1 a1, A2 a2, A3 a3, A4 a4, A5 a5, A6 a6)
{
    typedef typename _bi::list_av_6<A1, A2, A3, A4, A5, A6>::type list_type;
    return _bi::bind_t<R, F, list_type>(f, list_type(a1, a2, a3, a4, a5, a6));
}

更多源代码见这里、这里。

小小的boost.bind,实现代码逾两千行,其间重复代码无数。用了variadic-templates,实现不过百行。

BTW. variadic templates在C++大会上一次性几乎全数投票通过。lambda能不能进标准则要看几个提案者的工作。目前还没有wording出来。不过只要出了wording想必也会像variadic templates那样压倒性通过的。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
级别: 管理员
发帖
8532
金币
2762
威望
3231
贡献值
0
元宝
0
只看该作者 9楼 发表于: 2011-08-15
《C++0x漫谈》系列之:Auto的故事
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,比如上面这个,如果写成:

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 word。C++自有模板开始就支持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++09中auto的故事。

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

下期预告
本来这篇是要写lambda function的,因为最近scott meyers在On Software的一个访谈里提到他认为C++09最有用的特性不是concept而是lambda。所以…
等不及的可以先看这里、这里,这两个提案。也可以到g9老大的blog上看这里、这里对java中的lambda function(java里叫closure)的讨论,和对javascript里的。或者OCaml.cn上的这里。或者java closure的主要实现者的blog。
QQ: 378890364 微信:wwtree(省短信费) 紧急事宜发短信到0061432027638  本站微博:http://t.qq.com/wwtree QQ群:122538123
描述
快速回复

您目前还是游客,请 登录注册
批量上传需要先选择文件,再选择上传