首页 > 编程 > C > 正文

结构成员对齐与序列化

2023-06-09 12:08:02
字体:
来源:转载
供稿:网友

在许多广泛应用的程序库,我们会看到类似 #pragma pack(push, 4) 等这样的标示。因为用户会任意更改他们的结构成员对齐选项,对于先于这些内容创建的程序库来说,不能确保一定的内存布局将可能在预先书写的一些数据访问模块上导致错误,或者根本不可能实现。

我在实现一种C++ 类的实例的序列化工具时,依赖了内存布局。我知道市面上很多“序列化”工具允许更为广泛的通信用途,但是它们也是用起来最麻烦的,有很多限制条件。我实现的序列化工具用意很明显,为特定运行模块提供便捷高效的持久化存储能力。

为了提供感性的认识,提供了一个使用这个序列化工具的类型定义。

class StorageDoc
        : public SerialOwner
{
public:
        Serializable(StorageDoc);

        char c;
        int i;
        SerialString str;
};

它继承自 SerialOwner,它声明了 Serializable,隐含着实现了一些接口,为基类访问当前类型信息提供帮助。这是较早书写的一种方案,现在我会改用模板以便在编译时建立类型信息,不过原理完全一样。

现在,StorageDoc 当中的内存布局需要可确定的,但是用户会选择不同的结构成员对齐选项,为此需要设定一个结构成员对齐的“子域”,完成这项能力的伪指令是 #pragma pack。

#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n  )

1)当选用 show,则添加一条警告信息,指示当前编译域内的对齐属性
2)仅仅设置 n,则重写编译器选项 /Zp,并影响到此声明以下的同一个编译单元内的所有结构定义
3)push 以及 pop 管理了一组“子域”堆栈,可以不断加深嵌套
4)identifier 命名了堆栈上的对齐项,以便在特定需求中弹出合适的项目

以下是使用的注意事项:

1)不论何时,#pragma pack() 总是恢复到 /Zp 的预设值,即使处于 push 的“子域”
2)#pragma pack(push) 未指定对齐值,则不改变
3)#pragma pack(pop) 可指定对齐值出栈后的设置值,若不指定则按嵌套等级还原,直至 /Zp 预设值

综上,#pragma pack(pop) 总是能正确回退到上一个作用域,不管该作用域通过 #pragma pack(n) 声明或者 #pragma pack(push, n)。而 #pragma pack() 总是取预设值。对于用户事先指定了一个“子域”,并在其中引入了一个使用 #pragma pack(n) - #pragma pack() 对而非堆栈形式来声明局部结构成员对齐的头文件,会使用户非常困惑。<d3d9types.h> 就是这样做的。

当我们为程序库编译运行时,有一些类型要求严ge地遵守内存布局,比如一些硬件允许我们传入的数据就需要这么做,就可以把它们限定起来:

#pragma pack(push, 8)

#include "Chain.h"
#include "ByteQueue.h"
#include "SerialOwner.h"
#include "SerialUser.h"
#include "SerialString.h"
#include "SerialStream.h"

#pragma pack(pop)

事情再回到序列化上面,用户会多次尝试编译他们的序列化应用模块,并指望前一次编译之后运行所产生的文件仍然是可用的,所以还需要在用户文件当中明确所选用的对齐值,并一旦确定就不再更改:

#pragma pack(push, 8)
class StorageDoc
        : public SerialOwner
{
public:
        Serializable(StorageDoc);

        char c;
        int i;
        SerialString str;
};
#pragma pack(pop)

并使用它们:

StorageDoc doc;

doc.Load(t("doc.bin"));
std::cout << doc.str.Get() << std::endl;

doc.str = ss.str();
std::cout << doc.str.Get() << std::endl;
doc.Save(t("doc.bin"));

这就是全部了,但是正如以上提到的,不仅仅在序列化上,和硬件、链接库的通信也可能存在严ge的内存布局的要求,如果你在项目设计上遭遇这些困惑,那么现在就可以立即动手解决它们。

如果对本文提到的序列化能力感兴趣的话,可以到以下链接了解详情:

http://code.google.com/p/los-lib/source/browse/

目录是:

svn/trunk/Inc/Los/

文件分别是:

_ISerialUser.h
ByteQueue.h
Chain.h
Serialization.h
SerialOwner.h
SerialStream.h
SerialString.h
SerialUser.h

不过在本文发布之时,以上文件所处版本没有针对结构成员对齐选项进行修改,但并不影响阅读。

* 补充一(2009-1-18 02:41)

联合以及结构的结构成员对齐异常

class Tick
{
        static int _StaticID;

        __int64 _StartLI; // __alignof(LARGE_INTEGER) != __alignof(__int64)
        __int64 _CurrentLI;
        __int64 _Frequency;

        int _ID;
        clock_t _Start;
        clock_t _Current;

        bool _Stop;
        bool _HighPerformance;
...
}

LARGE_INTEGER 是分别对应两个 32bit 以及一个 64bit 类型的联合,奇怪的是随着全局对齐选项的修改,LARGE_INTEGER 类型本身的请求对齐 __alignof(LARGE_INTEGER) 将取联合的成员的最大者同全局对齐选项的最小值,也就是说,当 /Zp 设置为 2,那么 LARGE_INTEGER 也将仅承诺在 2 字节边界上对齐,多么不幸啊。当然如果将这个类型纳入 #pragma pack 的限定域那就什么问题都没有了,不管联合的对齐算法多么的古怪,只要保证不修改所需的对齐值那将总是能获得确定的内存布局。

不过正如上面的代码列出的,我使用了 __int64 代替了 LARGE_INTEGER 的工作,并在请求 Win32 API 的接口上强制指针转型,使用的时候亦如此,但若访问联合成员刚好为 __int64 类型则直接使用便可。这种方式没有获得额外的好处,算是一种抗议的行为,并且让后来的阅读者有机会了解到这个见不得光的问题。

_HighPerformance = ::QueryPerformanceFrequency((LARGE_INTEGER*)&_Frequency) != 0;

当然作为严肃的代码写作者,也许你将在不止一处使用到 LARGE_INTEGER,为此我也不拒绝使用如下格式:

#pragma pack(push, 8)
#include <windows.h>
#pragma pack(pop)

它可保证你万无一失。

作为对比,FILETIME 有如下定义:

typedef struct _FILETIME
    {
    DWORD dwLowDateTime;
    DWORD dwHighDateTime;
    }   FILETIME;

且不论它所需的可能的最大结构成员对齐为 4,它也将伴随着 /Zp 的更改而变动。因此,在不同的选项的影响下:

__alignof(LARGE_INTEGER) != __alignof(FILETIME) != __alignof(__int64)

有些人可能要指责会发生这样的问题纯粹是用户在玩弄“结构成员对齐选项”而导致的,我真希望他能够读一读这篇文章。

* 补充二(2009-1-18 02:41)

D3D 与用户定义结构的协调

class VertexXYZ_N_T1
{
public:
        float x, y, z;
        float normal_x, normal_y, normal_z;
        float u, v;
        DeviceBitmap* bitmap;
        Material* material;
        float temp_val;

        static const int FVF = D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1;
};

这是一个自定义顶点结构,它的最大成员字节数为 4,所有的成员也都是 4 字节边界,不论作何选项,始终保持紧凑存储,若其中一个成员扩展为 8 字节,那么伴随着选项的更改,VertexXYZ_N_T1 要求的对齐边界可导致部分空洞,从而同硬件所需的顶点缓存数据布局存在出入,我不追究硬件是否使用 double 值,但是现在就应当使用

#pragma pack(push, 4)
...
#pragma pack(pop)

加以限定。

我还定义了 Matrix, Material, Vector3, Colorf 等类型,如果要使得这些数据同 D3D, D3DX 的相应类型在内存上兼容的,也是需要限定的。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表