第八章 融入实际应用
8.1 对齐支持
8.1.1 数据对齐
c++可以通过sizeof查询数据的长度,但是没有对对齐方式有关的查询或者设定进行标准化。c++11标准定义的alignof函数可以查看数据的对齐方式。
现在的计算机通常会支持许多向量指令,4组8字节的浮点数据,很有潜力改造为能直接操作的向量数据,而默认的对齐方式是8字节的,我们最好能将其对齐在32字节的地址边界上。需要利用c++11新提供的修饰符alignas来重新设定对齐方式。
8.1.2 c++11的alignof和alignas
c++11在新标准中为了支持对齐,主要引入了两个关键字:操作符alignof、对齐描述符alignas。
alignof的操作数表示一个定义完整的自定义类型或者内置类型或者变量,返回值是一个std::size_t类型的整型常量。
alignas既可以接受常量表达式,也可以接受类型作为参数。常量表达式的结果必须是以2的自然数幂次作为对齐值。值越大,表示对齐要求越高。
c++11标准中规定了一个基本对齐值,一般情况下等于平台上支持的最大标量类型数据的对齐值(通常为long double)。我们可以通过alignof(std::max_align_t)来查询其值。设定超过标准对齐的做法叫做扩展对齐。
对齐描述符可以作用于各种数据,可以修饰变量、类的数据成员等,而位域以及用register声明的变量则不可以。
c++11stl库还内建了std::align函数来动态地根据指定的对齐方式调整数据块的位置。
该函数在ptr指向的大小为space的内存中进行对齐方式的调整,将ptr开始的size大小的数据调整为按alignment对齐。
c++11还在标准库中提供了aligned_storage及aligned_union供程序员使用。
8.2 通用属性
8.2.1 语言扩展到通用属性
编译器厂商或组织为了满足编译器客户的需求,设计出了一系列的语言扩展来扩展语法,最常见的就是属性(attribute)。属性是对语言中的实体对象(比如函数、变量、类型等)附加一些的额外注解信息,其用来实现一些语言及非语言层面的功能,或是实现优化代码等的一种手段。
不同的编译器有不同的属性语法,比如对于g++,属性是通过GNU的关键字__attribute__来声明的:__attribute__((attribute-list))。而Windows平台上,则使用关键字__declspec:__declspec(extended-decl-modifier)。c++11的通用属性采用了不一样的设计。
8.2.2 c++11的通用属性
c++11语言中的通用属性使用了左右双中括号的形式:[[attribute-list]]。这样设计的好处是既不会消除语言添加或者重载关键字的能力,又不会占用用户空间的关键字的名字空间。
语法上,c++11的通用属性可以作用于类型、变量、名称、代码块等。对于作用于声明的通用属性,既可以写在声明的开始处,也可以写在声明的标识符之后。而对于作用于整个语句的通用属性,则应该写在语句起始处。而出现在以上两种规则描述的位置之外的通用属性,作用于哪个实体跟编译器具体的实现有关。
现有c++11标准中,只预定义了两个通用属性,分别是[[noreturn]]和[[carries_dependency]]。
8.2.3 预定义的通用属性
[[noreturn]]是用于标识不会返回的函数的。不会返回的函数在被调用完成后,后续代码不会再被执行。主要用于标识哪些不会将控制流返回给原调用函数的函数。典型的例子是:有终止应用程序语句的函数、有无限循环语句的函数、有异常排除的函数等。通过该属性能帮助编译器产生更好的告警信息,同时编译器也可以做更多的诸如死代码消除、免除为函数调用者保存一些特定寄存器等代码优化工作。
[[carries_dependency]]则跟并行情况下的编译器优化有关。事实上,[[carries_dependency]]主要是为了解决弱内存模型平台上使用memory_order_consume内存顺序枚举问题。该通用属性既可以标识函数参数,又可以标识函数的返回值。当标识函数的参数时,它表示数据依赖随着参数传递进入函数,即不需要产生内存栅栏。而当标识函数的返回值时,它表示数据依赖随着返回值传递出函数,同样也不需要产生内存栅栏。
8.3 Unicode支持
8.3.1 字符集、编码和Unicode
通常,我们称ISO/Unicode所定义的字符集为Unicode。在Unicode中,每个字符占据一个码位(code point)。Unicode字符集共定义了1114112个这样的码位,使用从0到10FFFF的十六进制数唯一地表示所有的字符。由于计算机存储数据通常是以字节为单位,而且出于兼容之前的ASCII、大数小段数段、节省存储空间等诸多原因,通常需要一种具体的编码方式来对字符码位进行存储。比较常见的基于Unicode字符集的编码方式有UTF-8、UTF-16及UTF-32。
以UTF-8为例,其采用了1-6字节的变长编码方式编码Unicode,英文通常使用1字节表示,且与ASCII是兼容的,而中文常用3字节进行表示。UFT-8编码由于较为节约存储空间,因而使用得比较广泛。
在中文地区我们还有一些常见的字符集及其编码方式,GB2312、Big5是其中影响最大、使用最广泛的两种。
8.3.2 c++11中的Unicode支持
c++98标准中,为了支持Unicode,定义了宽字符的内置类型wchar_t。在Windows上,多数wchar_t被实现为16位宽,而Linux上则被实现为32位。C++98标准定义中,wchar_t的宽度是由编译器实现所决定的,理论上可以是8位、16位或者32位。带来的问题是包含wchar_t的代码通常不可移植。
c++11引入两种新的内置数据类型来存储不同编码长度的Unicode数据:
char16_t:用来存储UTF-16编码的Unicode数据。
char32_t:用来存储UTF-32编码的Unicode数据。
至于UTF-8编码的Unicode数据,c++11还是使用8字节宽度的char类型的数组来保存。
此外,c++11还定义了一些常量字符串的前缀,这些前缀可以让编译器使字符串按照前缀类型缠身数据:
u8表示为UTF-8编码,u表示为UTF-16编码,U表示为UTF-32编码。(wchar_t的前缀是L)。
按照c/c++的规则,连续在代码中声明多个字符串字面量,则编译器会自动将其连接起来。一旦连续声明多个字符串字面量中的某一个是前缀的,则不带前缀的字符串字面量会被认为与带前缀的字符串字面量是同类型的。
对于Unicode编码字符串的书写,c++11中还规定了一些简明的方式,即在字符串中用‘\u’加4个十六进制数编码的Unicode码位(UTF-16)来标识一个Unicode字符。
相比于定长编码的UTF-16,变长编码的UTF-8的优势在于支持更多的Unicode码位,而且也没有大数大小端问题。不过不能直接数组式访问是UTF-8的最大缺点。此外c++11为char16_t和char32_t分别配备了u16string和u32string等字符串类型,却没有u8string。
现有的c++编程中,总是倾向于在I/O读写的时候采用UTF-8编码,内存中一直操作的是定长的Unicode编码。
8.3.3 关于Unicode的库支持
c11库中新增了一些编码转换函数来完成各种Unicode编码间的转换。
c++对字符转换的支持则稍微复杂一些,新方法需要源自于c++的locale机制的支持。在c++中,通常情况下,locale描述的是一些必须知道的区域特征,如程序运行的国家/地区的数字符号、日期表示、钱币符号,采用的字符集和编码等。
地区的某个特征叫做facet。c++中常见的facet除去num_get/num_put(数值存取)、money_get/money_put等外,还有一种就是codecvt。codecvt能够完成从当前locale下多字符编码字符串到多种Unicode字符编码转换。
c++标准中,规定一共需要实现4种这样的codecvt facet:
考虑到Unicode在序列化存储的时候很少是UTF-16或者UTF-32的,所以从实际出发,没有在c++11中提供支持该功能的u16ifstream、u32ofstream等。
8.4 原生字符串字面量
原生字符串使用用户书写的字符串所见即所得,不再需要如'\t'、'\n'等控制字符来调整字符串中的格式。
c++11中只需要在字符串前加入前缀字母R,并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串了。
对于Unicode的字符串,也可以使用相同的方式声明,前缀分别为u8R,uR,UR。使用原生字符串的话,转义字符就不能使用了。原生字符串字面量也遵从连接规则。