《深入理解C++11:C++ 11新特性解析与应用》——1.4 C++特性一览

1.4 C++特性一览

接下来,我们会一窥C++11中的各种特性,了解它们的来历、用途、特色等。可能这部分对于还没有开始阅读正文的读者来说有些困难。如果有机会,我们建议读者在读完全书后再回到这里,这也是全书最好的总结。

1.4.1 稳定性与兼容性之间的抉择

通常在语言设计中,不破坏现有的用户代码和增加新的能力,这二者是需要同时兼顾的。就像之前的C一样,如今C++在各种代码中、开源库中,或用户的硬盘中都拥有上亿行代码,那么当C++标准委员会要改变一个关键字的意义,或者发明一个新的关键字时,原有代码就很可能发生问题。因为有些代码可能已经把要加入的这个准关键字用作了变量或函数的名字。

语言的设计者或许能够完全不考虑兼容性,但说实话这是个丑陋的做法,因为来自习惯的力量总是超乎人的想象。因此C++11只是在非常必要的情况下才引入新的关键字。WG21 在加入这些关键字的时候非常谨慎,至少从谷歌代码搜索(Google Code Search)的结果看来,这些关键字没有被现有的开源代码频繁地使用。不过谷歌代码搜索只会搜索开源代码,私人的或者企业的代码库(codebase)是不包含在内的。因此这些数据可能还有一定的局限性,不过至少这种方法可以避免一些问题。而WG21中也有很多企业代表,他们也会帮助WG21确定这些关键字是否会导致自己企业代码库中代码不兼容的问题。

C++11的新关键字如下:

《深入理解C++11:C++ 11新特性解析与应用》——1.4 C++特性一览
《深入理解C++11:C++ 11新特性解析与应用》——1.4 C++特性一览

这些新关键字都是相对于C++98/03来说的。当然,引入它们可能会破坏一些C++98/03代码,甚至更为糟糕的是,可能会悄悄地改变了原有C++98/03程序的目的。static_assert就是这样一个例子。为了降低它与已有程序变量冲突的可能性,WG21将这个关键字的名字设计得很长,甚至还包含了下划线,可以说命名丑得不能再丑了,不过在一些情况下,它还是会发生冲突,比如:

static_assert(4<=sizeof(int), "error:small ints");

这行代码的意图是确定编译时(不是运行时)系统的int整型的长度不小于4字节,如果小于,编译器则会报错说系统的整型太小了。在C++11中这是一段有效的代码,在C++98/03中也可能是有效的,因为程序员可能已经定义了一个名为static_assert的函数,以用于判断运行时的int整型大小是否不小于4。显然这与C++11中的static_assert完全不同。

实际上,在C++11中还有两个具有特殊含义的新标识符:override、final。这两个标识符如何被编译器解释与它们所在的位置有关。如果这两个标识符出现在成员函数之后,它们的作用是标注一个成员函数是否可以被重载。不过读者实际上也可以在C++11代码中定义出override、final这样名称的变量。而在这样的情况下,它们只是标识了普通的变量名称而已。

我们主要会在第2章中看到相关的特性的描述。

1.4.2 更倾向于使用库而不是扩展语言来实现特性

相比于语言核心功能的稳定,库则总是能随时为程序员提供快速上手的、方便易用的新功能。库的能量是巨大的,Boost和一些公司私有的库(如Qt、POOMA)的快速成长就说明了这一点。而且库有一个很大的优势,就是其改动不需要编译器实现新特性(只要接口保持一致即可),当然,更重要的是库可以用于支持不同领域的编程。这样一来,通常读者不需要非常精通C++就能使用它们。

不过这些优点并不是被广泛认可的。*的语言爱好者总是觉得功能加入语言特性,由编译器实现了才是王道,而库只是第二选择。不过WG21的看法跟他们相反。事实上,如果可能,WG21会尽量将一个语言特性转为库特性来实现。比较典型的如C++11中的线程,它被实现为库特性的一部分:std::thread,而不是一个内置的“线程类型”。同样的,C++11中没有内置的关联数组(associative array)类型,而是将它们实现为如std::unorder_map这样的库。再者,C++11也没有像其他语言一样在语言核心部分加入正则表达式功能,而是实现为 std::regex库。这样一来,C++语言可以尽量在保持较少的核心语言特性的同时,通过标准库扩大其功能。

从传统意义上讲,库可能是通过提供头文件来实现的。当然,有些时候库的提供者也会将一些实现隐藏在二进制代码库存档(archive)文件中。不过并非所有的库都是通过这样的方式提供的。事实上,库也有可能实现于编译器内部。比如C++11中的原子操作等许多内容,就通常不是在头文件或库存档中实现的。编译器会在内部就将原子操作实现为具体的机器指令,而无需在稍后去链接实实在在的库进行存档。而之所以将原子操作的内容放在库部分,也是为了满足将原子操作作为库实现的*。从这个意义上讲,原子操作并非纯粹的“库”,因此也被我们选择性地纳入了本书的讲解中。

1.4.3 更倾向于通用的而不是特殊的手段来实现特性

如我们说到的,如果将无数互不相关的小特性加入C++中,而且不加选择地批准通过,C++将成为一个令人眼花缭乱的“五金店”,不幸的是,这个五金店的产品虽然各有所长,凑在一起却是一盘散沙,缺乏战斗力。所以WG21更希望从中抽象出更为通用的手段而不是加入单独的特性来“练成”C++11的“十八般武艺”。

显式类型转换操作符是一个很好的例子。在C++98/03中,可以用在构造函数前加上explicit关键字来声明构造函数为显式构造,从而防止程序员在代码中“不小心”将一些特定类型隐式地转换为用户自定义类型。不过构造函数并不是唯一会导致产生隐式类型转换的方法,在C++98/03中类型转换操作符也可以参与隐式转换,而程序员的意图则可能只是希望类型转换操作符在显式转换时发生。这是C++98/03的疏忽,不过在C++11中,我们已经可以做到这点了。

其他的一些新特性,比如继承构造函数、移动语义等,在本书的第3章中我们均会涉及。

1.4.4 专家新手一概支持

如果C++只是适合专家的语言,那它就不可能是一门成功的语言。C++中虽然有许多专家级的特性,但这并不是必须学习的。通常程序员只需要学习一定的知识就可以使用C++。而在C++11中,从易用的角度出发,修缮了很多特性,也铲除了许多带来坏声誉的“毒瘤”,比如一度被群起而攻之的“毒瘤”—双右尖括号。在C++98/03中,由于采用了最长匹配的解析规则(maximal munch parsing rule),编译器会在解析符号时尽可能多地“吸收”符号。这样一来,在模板解析的时候,编译器就会将原本是“模板的模板”识别为右移,并“理直气壮”地抛出一条令人绝望的错误信息:模板参数中不应该存在的右移。如今这个问题已经在C++11中被修正。模板参数内的两个右尖括号会终结模板参数,而不会导致编译器错误。当然从实现上讲,编译器只需要在原来报错的地方加入一些上下文的判断就可以避免这样的错误了。比如:

vector<list<int>> veclist: //C++11中有效,C++98/03中无效

另一个C++11易于上手的例子则是统一初始化语法的引入。C++继承了C语言中所谓的“集合初始化语法”(aggregate initialization syntax,比如a[] = {0, 1,};),而在设计类的时候,却只定义了形式单一的构造函数的初始化语法,比如A a(0, 1)。所以在使用C++98/03的时候,编写模板会遇到障碍,因为模板作者无法知道模板用户会使用哪种类型来初始化模板。对于泛型编程来说,这种不一致则会导致不能总是进行泛型编程。而在C++11中,标准统一了变量初始化方法,所以模板作者可以总是在模板编写中采用集合初始化(初始化列表)。进一步地,集合初始化对于类型收窄还有一定的限制。而类型收窄也是许多让人深夜工作的奇特错误的源头。因此在C++11中使用了初始化列表,就等同于拥有了防止收窄和泛型编程的双重好处。

读者可以在第4章看到C++11是如何增进语言对新手的支持的。

1.4.5 增强类型的安全性

绝对的类型安全对编程语言来说几乎是不可能达到的,不过在编译时期捕捉更多的错误则是非常有益的。在C++98/03中,枚举类会退化为整型,因此常会与其他的枚举类型混淆。这个类型的不安全根源还是在于兼容C语言。在C中枚举用起来非常便利,在C++中却是类型系统的一个大“漏勺”。因此在C++11中,标准引入了新的“强类型枚举”来解决这个问题。

enum class Color { red, blue, green };
int x = Color::red;    //C++98/03中允许,C++11中错误:不存在Color->int的转换
Color y = 7;    //C++98/03中,C++11中错误:不存在int->Color conversion的转换
Color z = red;    //C++98/03中允许,C++11中错误:red不在作用域内
Color c = Color::red;    //C++98/03中错误,C++11中允许

在第5章中,我们会详细讲解诸如此类能够增强类型安全的C++11特性。

1.4.6 与硬件紧密合作

在C++编程中,嵌入式编程是一个非常重要的领域。虽然一些方方圆圆的智能设备外表光鲜亮丽,但是植根于其中的技术基础也常常会是C++。在C++11中,常量表达式以及原子操作都是可以用于支持嵌入式编程的重要特性。这些特性对于提高性能、降低存储空间都大有好处,比如ROM。

C++98/03中也具备const类型,不过它对只读内存(ROM)支持得不够好。这是因为在C++中const类型只在初始化后才意味着它的值应该是常量表达式,从而在运行时不能被改变。不过由于初始化依旧是动态的,这对ROM设备来说并不适用。这就要求在动态初始化前就将常量计算出来。为此标准增加了constexpr,它让函数和变量可以被编译时的常量取代,而从效果上说,函数和变量在固定内存设备中要求的空间变得更少,因而对于手持、桌面等用于各种移动控制的小型嵌入式设备(甚至心率调整器)的ROM而言,C++11也支持得更好。

在C++11,我们甚至拥有了直接操作硬件的方法。这里指的是C++11中引入的原子类型。C++11通过引入内存模型,为开发者和系统建立了一个高效的同步机制。作为开发者,通常需要保证线程程序能够正确同步,在程序中不会产生竞争。而相对地,系统(可能是编译器、内存系统,或是缓存一致性机制)则会保证程序员编写的程序(使用原子类型)不会引入数据竞争。而且为了同步,系统会自行禁止某些优化,又保证其他的一些优化有效。除非编写非常底层的并行程序,否则系统的优化对程序员来讲,基本上是透明的。这可能是C++11中最大、最华丽的进步。而就算程序员不乐意使用原子类型,而要使用线程,那么使用标准的互斥变量mutex来进行临界区的加锁和开锁也就够了。而如果读者还想要疯狂地挖掘并行的速度,或试图完全操控底层,或想找点麻烦,那么无锁(lock-free)的原子类型也可以满足你的各种“野心”。内存模型的机制会保证你不会犯错。只有在使用与系统内存单位不同的位域的时候,内存模型才无法成功地保证同步。比如说下面这个位域的例子,这样的位域常常会引发竞争(跨了一个内存单元),因为这破坏了内存模型的假定,编译器不能保证这是没有竞争的。

struct {int a:9; int b:7;}

不过如果使用下面的字符位域则不会引发竞争,因为字符位域可以被视为是独立内存位置。而在C++98/03中,多线程程序中该写法却通常会引发竞争。这是因为编译器可能将a和b连续存放,那么对b进行赋值(互斥地)的时候就有可能在a没有被上锁的情况下一起写掉了。原因是在单线程情况下常被视为普通的安全的优化,却没有考虑到多线程情况下的复杂性。C++11则在这方面做出了较好的修正。

struct {char a; char b;}

与硬件紧密合作的能力使得C++可以在任何系统编程中继续保持领先的位置,比如说构建设备驱动或操作系统内核,同时在一些像金融、游戏这样需要高性能后台守护进程的应用中,C++的参与也会大大提升其性能。

我们会在第6章看到相关特性的描述。

1.4.7 开发能够改变人们思维方式的特性

C++11中一个小小的lambda特性是如何撬动编程世界的呢?从一方面讲,lambda只是对C++98/03中带有operator()的局部仿函数(函数对象)包装后的“语法甜点”。事实上,在C++11中lambda也被处理为匿名的仿函数。当创建lambda函数的时候,编译器内部会生成这样一个仿函数,并从其父作用域中取得参数传递给lambda函数。不过,真正会改变人们思维方式的是,lambda是一个局部函数,这在C++98/03中我们只能模仿实现该特性。此外,当程序员开始越来越多地使用C++11中先进的并行编程特性时,lambda会成为一个非常重要的语法。程序员将会发现到处都是奇怪的“lambda笑脸”,即;},而且程序员也必须习惯在各种上下文中阅读翻译lambda函数。顺带一提,lambda笑脸常会出现在每一个lambda表达式的终结部分。

另一个人们会改变思维方式的地方则是如何让一个成员函数变得无效。在C++98/03中,我们惯用的方法是将成员函数声明为私有的。如果读者不知道这种方法的用意,很可能在阅读代码的时候产生困惑。不过今天的读者非常幸运,因为在C++11中不再需要这样的手段。在C++11中我们可以通过显式默认和删除的特性,清楚明白地将成员函数设为删除的。这无疑改变了程序员编写和阅读代码的方式,当然,思考问题的方式也就更加直截了当了。

我们会在第7章中看到相关特性的描述。

1.4.8 融入编程现实

现实世界中的编程往往都有特殊的需求。比如在访问因特网的时候我们常常需要输入URL,而URL通常都包含了斜线“/”。要在C++中输入斜线却不是件容易的事,通常我们需要转义字符“/”的配合,否则斜线则可能被误认为是除法符号。所以如果读者在写网络地址或目录路径的时候,代码最终看起来就是一堆倒胃口的反斜线的组合,而且会让内容变得晦涩。而C++11中的原生字符串常量则可免除“转义”的需要,也可以帮助程序员清晰地呈现网络地址或文件系统目录的真实内容。

另一方面,如今GNU的属性(attribute)几乎无所不在,所有的编译器都在尝试支持它,以用于修饰类型、变量和函数等。不过__attribute__((attribute-name))这样的写法,除了不怎么好看外,每一个编译器可能还都有它自己的变体,比如微软的属性就是以__declspec打头的。因此在C++11中,我们看到了通用属性的出现。

不过C++11引入通用属性更大的原因在于,属性可以在不引入额外的关键字的情况下,为编译提供额外的信息。因此,一些可以实现为关键字的特性,也可以用属性来实现(在某些情况下,属性甚至还可以在代码中放入程序供应商的名字,不过这样做存在一些争议)。这在使用关键字还是将特性实现为一个通用属性间就会存在权衡。不过最后标准委员会认为,在现在的情况下,在C++11中的通用属性不能破坏已有的类型系统,也不应该在代码中引起语义的歧义。也就是说,有属性的和没有属性的代码在编译时行为是一致的。所以C++11标准最终选择创建很少的几个通用属性—noreturn和carrier_dependency(其实final、override也一度是热门“人选”)。

属性的真正强大之处在于它们能够让编译器供应商创建他们自己的语言扩展,同时不会干扰语言或等待特性的标准化。它们可以被用于在一些域内加入特定的“方言”,甚至是在不用pragma语法的情况下扩展专有的并行机制(如果读者了解OpenMP,对此会有一些体会)。

我们将在第8章中看到相关的描述。

上一篇:极简教程 | Spring Boot整合Elasticsearch实现数据搜索引擎


下一篇:3分钟实现SpringBoot集成RabbitMQ,实现消息队列服务!