8.1.2 C++11的alignof和alignas

如同我们在上一节中看到的,C++11在新标准中为了支持对齐,主要引入两个关键字:操作符alignof、对齐描述符(alignment-specifier)alignas。

操作符alignof的操作数表示一个定义完整的自定义类型或者内置类型或者变量,返回的值是一个std::size_t类型的整型常量。如同sizeof操作符一样,alignof获得的也是一个与平台相关的值。我们可以看看代码清单8-4所示的例子。

代码清单8-4


include <iostream>

using namespace std;

class InComplete;

struct Completed{};

int main(){

int a;

long long b;

auto&c=b;

char d[1024];

//对内置类型和完整类型使用alignof

cout<<alignof(int)<<endl//4

<<alignof(Completed)<<endl;//1

//对变量、引用或者数组使用alignof

cout<<alignof(a)<<endl//4

<<alignof(b)<<endl//8

<<alignof(c)<<endl//8,与b相同

<<alignof(d)<<endl;//1,与元素要求相同

//本句无法通过编译,Incomplete类型不完整

//cout<<alignof(Incomplete)<<endl;

}

//编译选项:g++ -std=c++11 8-1-4.cpp


使用alignof很简单,基本上没有什么特别的限制。不过在代码清单8-4中,类型定义不完整的class InComplete是无法通过编译的。其他的规则则基本跟大多数人想象的相同:引用c与其引用的数据b对齐值相同,数组的对齐值由其元素决定。

我们再来看看对齐描述符alignas。事实上,alignas既可以接受常量表达式,也可以接受类型作为参数,比如


alignas(double)char c;


也是合法的描述符。其使用效果跟


alignas(alignof(double))char c;


是一样的。

注意 在C++11标准之前,我们也可以使用一些编译器的扩展来描述对齐方式,比如GNU格式的attribute((aligned(8)))就是一个广泛被接受的版本。

我们在使用常量表达式作为alignas的操作符的时候,其结果必须是以2的自然数幂次作为对齐值。对齐值越大,我们称其对齐要求越高;而对齐值越小,其对齐要求也越低。由于2的幂次的关系,能够满足严格对齐要求的对齐方式也总是能够满足要求低的对齐值的。

在C++11标准中规定了一个“基本对齐值”(fundamental alignment)。一般情况下其值通常等于平台上支持的最大标量类型数据的对齐值(常常是long double)。我们可以通过alignof(std::max_align_t)来查询其值。而像我们在代码清单8-3中设定ColorVector对齐值到32字节(超过标准对齐)的做法称为扩展对齐(extended alignment)。不过即使使用了扩展对齐,也并非意味着程序员可以随心所欲。对于每个平台,系统能够支持的对齐值总是有限的,程序中如果声明了超过平台要求的对齐值,则按照C++标准该程序是不规范的(ill-formed),这可能会导致未知的编译时或者运行时错误。因此程序员应该定义合理的对齐值,否则可能会遇到一些麻烦。

对齐描述符可以作用于各种数据。具体来说,可以修饰变量、类的数据成员等,而位域(bit field)以及用register声明的变量则不可以。我们可以看看C++11标准中的这个例子,如代码清单8-5所示。

代码清单8-5


alignas(double)void f();//错误:alignas不能修饰函数

alignas(double)unsigned char c[sizeof(double)];//正确

extern unsigned char c[sizeof(double)];

alignas(float)

extern unsigned char c[sizeof(double)];//错误:不同对齐方式的变量定义

//编译选项:clang++8-1-5.cpp-c-std=c++11


对于代码清单8-5所示的例子,标准给出了建议的答案(如注释所示)。C++11标准建议用户在声明同一个变量的时候使用同样的对齐方式以免发生意外。不过C++11并没有规定声明变量采用了不同的对齐方式就终止编译器的编译。在编写本书时,clang++编译器对该例就没有终止编译,而是使用了最严格的对齐方式作为c的最终对齐方式。读者可以试试自己的编译环境,看一下编译器是如何处理的。

我们再来看一个例子,这个例子中我们采用了模板的方式来实现一个固定容量但是大小随着所用的数据类型变化的容器类型,如代码清单8-6所示。

代码清单8-6


include <iostream>

using namespace std;

struct alignas(alignof(double)*4)ColorVector{

double r;

double g;

double b;

double a;

};

//固定容量的模板数组

template<typename T>

class FixedCapacityArray{

public:

void push_back(T t){/在data中加入t变量/}

//…

//一些其他成员函数、成员变量等

//…

char alignas(T)data[1024]={0};

//int length=1024/sizeof(T);

};

int main(){

FixedCapacityArray<char> arrCh;

cout<<"alignof(char):"<<alignof(char)<<endl;

cout<<"alignof(arrCh.data):"<<alignof(arrCh.data)<<endl;

FixedCapacityArray<ColorVector>arrCV;

cout<<"alignof(ColorVector):"<<alignof(ColorVector)<<endl;

cout<<"alignof(arrCV.data):"<<alignof(arrCV.data)<<endl;

return 1;

}

//编译选项:clang++8-1-6.cpp-std=c++11


代码清单8-6修改自代码清单8-3,在本例中,FixedCapacityArray固定使用1024字节的空间,但由于模板的存在,可以实例化为各种版本。这样一来,我们可以在相同的内存使用量的前提下,做出多种类型(内置或者自定义)版本的数组。

如我们之前提到的一样,为了有效地访问数据,必须使得数据按照其固有特性进行对齐。对于arrCh,由于数组中的元素都是char类型,所以对齐到1就行了,而对于我们定义的arrCV,必须使其符合ColorVector的扩展对齐,即对齐到8字节的内存边界上。在这个例子中,起到关键作用的代码是下面这一句:


char alignas(T)data[1024]={0};


该句指示data[1024]这个char类型数组必须按照模板参数T的对齐方式进行对齐。

编译运行该例子后,可以在实验机上得到如下结果:


alignof(char):1

alignof(arrCh.data):1

alignof(ColorVector):32

alignof(arrCV.data):32


如果我们去掉alignas(T)这个修饰符,代码清单8-6的运行结果会完全不同,具体如下:


alignof(char):1

alignof(arrCh.data):1

alignof(ColorVector):32

alignof(arrCV.data):1


可以看到,由于char数组默认对齐值为1,会导致data[1024]数组也对齐到1。这肯定不是编写FixedCapacityArray的程序员愿意见到的。

事实上,在C++11标准引入alignas修饰符之前,这样的固定容量的泛型数组有时可能遇到因为对齐不佳而导致的性能损失(甚至程序错误),这给库的编写者带来了很大的困扰。而引入alignas能够解决这些移植性的困难,这可能也是C++标准委员会决定不再“隐藏”变量的对齐方式的原因之一。

C++11对于对齐的支持并不限于alignof操作符及alignas描述符。在STL库中,还内建了std::align函数来动态地根据指定的对齐方式调整数据块的位置。该函数的原型如下:


voidalign(std::size_t alignment,std::size_t size,void&ptr,std::size_t&space);


该函数在ptr指向的大小为space的内存中进行对齐方式的调整,将ptr开始的size大小的数据调整为按alignment对齐。我们可以看看代码清单8-7所示的这个例子。

代码清单8-7


include <iostream>

include <memory>

using namespace std;

struct ColorVector{

double r;

double g;

double b;

double a;

};

int main(){

size_t const size=100;

ColorVector*const vec=new ColorVector[size];

void*p=vec;

size_t sz=size;

voidaligned=align(alignof(double)4,size,p,sz);

if(aligned!=nullptr)

cout<<alignof(p)<<endl;

}


代码清单8-7尝试将vec中的内容按alignof(double)*4的对齐值进行对齐(不过在编写本书的时候,我们的编译器还没有支持std::align这个新特性,因此代码清单8-7仅供参考)。

事实上,C++11还在标准库中提供了aligned_storage及aligned_union供程序员使用。两者的原型如下:


template<std::size_t Len,std::size_t Align=/default-alignment/>

struct aligned_storage;

template<std::size_t Len,class…Types>

struct aligned_union;


aligned_storage的第一个参数规定了aligned_storage的大小,第二个参数则是其对齐值。我们可以通过代码清单8-8所示的这个例子说明它们的用途。

代码清单8-8


include <iostream>

include <type_traits>

using namespace std;

//一个对齐值为4的对象

struct IntAligned{

int a;

char b;

};

//使用aligned_storage使其对齐要求更加严格

typedef aligned_storage<sizeof(IntAligned),alignof(long double)>::type StrictAligned;

int main(){

StrictAligned sa;

IntAligned*pia=new(&sa)IntAligned;

cout<<alignof(IntAligned)<<endl;//4

cout<<alignof(StrictAligned)<<endl;//16

cout<<alignof(*pia)<<endl;//4

cout<<alignof(sa)<<endl;//16

return 0;

}

//编译选项:g++ -std=c++11 8-1-8.cpp


在代码清单8-8中,我们使用了一个placement new来使得StrictAligned存储了本来应该只需要按照4字节对齐的IntAligned对象。虽然StrictAligned对象sa的内容与IntAligned类型指针pia所指向的对象完全相同,但通过这样的声明,却产生了比*pia更加严格的类型对齐要求(本例中为16)。因此虽然最后IntAligned对象的对齐方式没有发生改变,但实际上却被更严格地对齐了。

有的时候,一个类型声明的代码较长,可能需要程序员上下翻页来阅读(虽然面向对象的规则并不推荐这样做,但在大型项目中,代码很长的类型声明并不少见),通常为了对齐,程序员不得不自己写一些填充来保证其大小。除了代码较难阅读外,每个系统上结构体、联合体的对齐规则也可能不一样,这在代码维护上是一种挑战。如果后加入类型成员的程序员没有注意到这里的对齐要求或者代码编写不慎,很可能会添加了导致对齐改变的代码(对齐变严格一般不是问题,但反之则可能是问题)。

改进的方法可以是使用alignas描述符,假如代码清单8-8中的IntAligned是一个代码很长的struct声明,那么对其使用一个alignas描述符就是一个可行的方法。不过很多时候,数据的声明是需要共享的,假如超长的IntAligned需要在支持和不支持alignas的编译环境下共享(典型的,要在老的C环境及C++11环境下共享头文件),那么使用aligned_storage则是一个可行的方法,因为aligned_storage可以在产生对象的实例时对对齐方式做出一定的保证。这无疑对“有历史”的代码的重用、维护很有意义。

aligned_union的用法也基本与此相同。只不过aligned_union使用了变长模板参数,程序员可以根据需要填入多种类型,最后aligned_union对象的对齐要求会是多个类型中要求最为严格的一个。

可以看到,在新的C++11标准中,对对齐方式的支持是全方面的,无论是查看(alignof)、设定(alignas),还是STL库函数(std::align)或是STL库模板类型(aligned_storage,aligned_union),程序员都可以找到对应的方法。这使得一些非标准的设定对齐方式的做法规范统一,真正满足程序员在可移植性上的要求。事实上,程序的可移植性还有很多的相关问题,接下来要讲到的通用属性,就与对齐方式有很多关联。