如何编写完全可移植的 4 字节字符常量的编译时初始化

How to write a compile-time initialisation of a 4 byte character constant that is fully portable

本文关键字:字符常量 编译 初始化 字节 可移植 何编写      更新时间:2023-10-16

(遗留)代码大致如下所示。

#define MAKEID(a,b,c,d) (((UInt32)a)<<24 | ((UInt32)b)<<16 
| ((UInt32)c)<<8 | ((UInt32)d) )
#define ID_FORM MAKEID('F','O','R','M')
//...
struct S { 
int id;
int x;
double d;
// other stuff
};
//...
S s;
socket.read(&s, sizeof(s));  // read network data over struct
if (s.id == ID_FORM) { }

该代码将字符流读入结构。已知 S.id 是一个 4 个字符的常量,例如流(网络)顺序中的"FORM"或"DATA",它决定了结构其余部分的布局。所有比较都是使用预定义常量的整数。

MAKEID 宏是大端序的,因为它将第一个(字符)参数放在最高有效字节处,这也是最低内存地址。宏的小端版本如下所示,将第一个(字符)参数放在最低有效字节处,现在是最低内存地址。

#define MAKEID(a,b,c,d) (((UInt32)d)<<24 | ((UInt32)c)<<16 
| ((UInt32)b)<<8 | ((UInt32)a) )

问题是如何重写它,使其在大端和小端架构上同样有效。

不,我不想同时编写两个宏并选择带有 #ifdef 的宏。代码中没有其他字节序依赖项,我不想在这里介绍一个。便携是必经之路。

不,我不想写函数。此常量用于函数无法到达的地方。我编写了一个可移植函数来初始化联合,但代码无法编译。

我正在寻找任何类型的可移植宏或模板来进行编译时初始化。

在回答注释时,这是真正的代码。它是网络协议的一部分,在大多数情况下,另一端负责字节序。这恰好是一个例外,其中另一端以网络字节顺序生成,并且此端在历史上被写为大端序作为 4 字节字符常量,如"FORM"。我需要一个点解决方案,而不是将字节序的概念传播到代码中的其他地方的解决方案。

您的MAKEID宏与字节序无关。它在大端和小端系统上的工作方式相同。

宏可能看起来是特定于大端序的,但C++中的移位和按位或运算都是根据它们对所操作值的结果定义的,而不是根据这些值的基础存储来定义的。
执行42 << 24可以保证将值42放在结果的最高有效 8 位中,无论字节顺序如何。对于按位或操作也是如此。这意味着MAKEID(0x12, 0x34, 0x56, 0x78)的结果总是0x12345678的,无论底层存储的字节顺序如何。

如果要生成一个整数,其基础存储始终具有相同的位模式(例如,0x12、0x34、0x56、0x78),那么您确实必须重新考虑您的方法。这样的整数在大端系统上的值0x12345678,在小端系统上0x78563412,在中端系统上可能0x56781234
但是,如果该位模式是通过使用特定字节顺序(例如大端序/网络字节顺序)定义的通信接口接收的,则如果希望接收系统正确解释这些值,并且包括四字节 ID 值,则必须将接收的任何多字节值转换为系统的本机字节顺序。

这就是为什么我在答案的早期版本中说,如果您发现在某些系统上(特别是那些系统的字节顺序与通信的字节顺序不匹配的系统),从流中读取的 ID 与MAKEID的结果不匹配,那么可能的罪魁祸首是反序列化代码。(反)序列化代码是考虑字节序的最重要位置。例如,在收到的字节上覆盖您期望的结构很容易,但如果可能存在字节顺序不匹配或填充差异,则这是错误的解决方案。

我向为我工作的程序员设置了同样的问题。在我们之间,我们提出了以下4个解决方案。我们将使用宏,也许在时间允许的情况下转换代码以使用其中一个函数。

unsigned int MakeId(char a, char b, char c, char d) {
char x[4] = { a, b, c, d };
return *(int*)x;
}
unsigned int MakeId(char a, char b, char c, char d) {
union {
char x[4];
int i;
} u = { a, b, c, d };
return u.i;
}
unsigned int MakeId(const char* s) { return *(int*)s; }
#define MAKEID(s) *(int*)(s);
#define FORM_ID MAKEID("Form")

在这种情况下,Stack Overflow的强大头脑没有实现。

与其在不同的机器上以不同的方式定义常量,不如处理收到的数据:

if (ntohl(s.id) == ID_FORM) { }

编辑: 为了避免编辑代码,您可以使用 htonl 来初始化ID_FORM:

#define ID_FORM htonl(MAKEID('F','O','R','M'))

这依赖于htonl是一个宏。通常都是这样。如果是这样,它通常使用您试图避免的相同条件来定义:http://www.jbox.dk/sanos/source/include/net/inet.h.html(例如)。

因此,如果您的系统上htonl不是宏,那么我看到的唯一理智的选择就是实际坚持#ifdef.

请记住,从现在开始,您的ID_FORM现在处于"网络字节序",而不是"主机字节序"。