为什么C++没有像有符号性那样的字节序修饰符?

58

(我猜这个问题适用于许多编程语言,但我选择以C++作为例子。)

为什么不能只写:

struct foo {
    little int x;   // little-endian
    big long int y; // big-endian
    short z;        // native endianness
};

如何指定特定成员、变量和参数的字节序?

与有符号性的比较

我理解一个变量的类型不仅决定了用多少字节来存储一个值,还决定了在执行计算时如何解释这些字节。

例如,以下两个声明都分配了一个字节,对于这两个字节,每个可能的8位序列都是一个有效值:

signed char s;
unsigned char u;
但是相同的二进制序列可能会有不同的解释,例如11111111在分配给s时表示-1,但在分配给u时表示255。当涉及到有符号和无符号变量在同一计算中时,编译器(大多数情况下)会处理正确的转换。

在我看来,字节顺序只是相同原理的一种变化:基于编译时关于将存储在其中的内存的信息的不同解释。这似乎在允许低级编程的类型语言中很明显。但是,这不是我知道的任何语言,也没有在网上找到任何相关讨论。

更新

我将尝试总结一些我在发帖后第一个小时得到的评论:

  1. 签名是严格二进制的(有符号或无符号),并且永远都是这样的,与字节顺序形成对比,后者也有两个众所周知的变体(big和little),还有一些较少知名的变体,例如mixed/middle endian。未来可能会发明新变体。
  2. 字节顺序对按字节访问的多字节值的访问方式很重要。除了字节顺序之外,还有许多因素影响多字节结构的内存布局,因此通常不建议使用这种访问方式。
  3. C++旨在针对抽象机器进行定位,并尽量减少对实现的假设。这个抽象机器没有任何字节顺序。

此外,我现在意识到,签名和字节顺序并不是完美的类比,因为:

  • 字节顺序仅定义了如何将某些东西表示为二进制序列,但不定义可以表示什么。两个big intlittle int将具有完全相同的值范围。
  • 签名定义了如何映射位和实际值之间的关系,但也影响可以表示的内容,例如-3无法用unsigned char表示,而(假设char具有8位)130无法用signed char表示。

因此,更改某些变量的字节顺序永远不会改变程序的行为(除了按字节访问),而更改签名通常会。


53
因为C++描述了一个抽象机器的行为,该机器没有字节序的概念。 - YSC
19
好的,我要介绍一个新的字节序概念——我称之为“反大端序”,它是大端序,但位顺序是反转的,而不是字节顺序。你想为了我的新架构改变整个语言吗? - UKMonkey
15
UKMonkey在使用讽刺的语气。他的观点是:字节序取决于计算机体系结构,所以每个人,包括UKMonkey在酸液中,都能设计出新的计算机体系结构。C++语言不应该考虑到在酸液中的SO(Stack Overflow)用户。 - YSC
7
我不明白这是一个显而易见的功能。它会解决什么问题? - molbdnilo
8
我认为可以说,符号的概念可以被视为抽象的,而字节序则非常具体且与实现相关。我想更好的比较是关于对齐规范。 - StoryTeller - Unslander Monica
显示剩余27条评论
9个回答

53

标准规定

[intro.abstract]/1:

本文件中的语义描述定义了一个参数化的非确定性抽象机器。该文件对符合规范的实现结构没有要求。特别地,它们不需要复制或模拟抽象机器的结构。相反,符合规范的实现需要仅模拟抽象机器的可观察行为,如下所解释。

C++无法定义字节序限定符,因为它没有字节序的概念。

讨论

关于符号位和字节序之间的区别,OP写道:

在我看来,字节序只是相同原理(符号位)的不同变体:基于存储它的内存的编译时信息,以不同的方式解释二进制模式。

我认为符号位既有语义方面的含义又有代表性方面的含义1[intro.abstract]/1暗示C++只关心语义,从未涉及有符号数在内存中应该如何表示2。实际上,“符号位”在C++规范中只出现过一次,是指实现定义的值。
另一方面,字节序只有代表性方面的含义:字节序不传达任何意义。

随着C++20的到来,std::endian出现了。它仍然是实现定义的,但让我们在不依赖基于未定义行为的老技巧的情况下测试主机的字节序。


1) 语义层面:有符号整数可以表示负值;代表性层面:例如,需要留出一位来传递正负号。
2) 同样地,C++从未描述过浮点数应如何表示,IEEE-754常被使用,但这是实现时做的选择,在任何情况下均受标准强制执行:[basic.fundamental]/8 "浮点类型的值表示是由实现定义的"


36
除了YSC的回答,让我们看一下您的示例代码,并考虑它可能旨在实现什么目标。
struct foo {
    little int x;   // little-endian
    big long int y; // big-endian
    short z;        // native endianness
};

您可能希望这可以精确地指定架构无关的数据交换(文件、网络等)布局。

但这不可能起作用,因为仍有几个未指定的事项:

  • 数据类型大小:如果需要,您必须分别使用little int32_tbig int64_tint16_t
  • 填充和对齐,无法在语言内严格控制:使用#pragma__attribute__((packed))或其他一些特定于编译器的扩展
  • 实际格式(1s-或2s补码符号、浮点类型布局、陷阱表示)

或者,您可能只想反映某些指定硬件的字节序 - 但是biglittle并不涵盖所有可能性(只涵盖两种最常见的情况)。

因此,该提案不完整(它无法区分所有合理的字节顺序排列),也不起作用(它无法实现其既定目标),而且还有其他缺点:

  • 性能

    从本机字节顺序更改变量的字节顺序应该要么禁用算术、比较等(因为硬件不能在此类型上正确执行它们),要么必须默默注入更多代码,创建本机排序的临时变量来处理。

    这里的论点不是手动转换为/从本机字节顺序更快,而是明确控制使其更容易最小化不必要的转换数量,并且比隐式转换更容易推断代码行为。

  • 复杂性

    现在,所有为整数类型重载或专门设计的内容都需要两倍的版本,以应对传递非本机字节顺序值的罕见事件。即使只是一个转发包装器(带有一些强制转换以将其翻译为/从本机排序),这仍然是很多代码,没有明显的好处。

反对更改语言以支持此功能的最后一个论点是您可以轻松地在代码中执行此操作。更改语言语法是一件大事,并且与类型包装器之类的东西相比没有明显的优势:

// store T with reversed byte order
template <typename T>
class Reversed {
    T val_;
    static T reverse(T); // platform-specific implementation
public:
    explicit Reversed(T t) : val_(reverse(t)) {}
    Reversed(Reversed const &other) : val_(other.val_) {}
    // assignment, move, arithmetic, comparison etc. etc.
    operator T () const { return reverse(val_); }
};

3
我不太明白"性能"如何成为一个论点。如果需要使用其他字节序,则必有原因。如果编程语言无法支持,那么需要手动编写代码。性能将是相等的。甚至,在语言内部实现版本可能会更快,因为编译器可以实现针对字节序转换的优化代码。关于复杂性:是的,如果这是一个新类型,那就很复杂。但如果该类型是相同的呢?就像const限定符一样。它可以工作,而没有严重的复杂性。 - geza
1
特别要注意的是,确实存在一些真实的机器,其中4字节整数既不是大端序也不是小端序(例如PDP/11混合端序和Prime S-mode,其中4字节整数为31位,可能还有其他情况)。 - Martin Bonner supports Monica
2
@MartinBonner:但是这个事实为什么会阻止C++有小/大端限定符呢?这将有助于人们与这些表示交互。这并不意味着我们必须在语言中拥有所有可能的整数表示。如果您想在PDP/11上解释PNG图片,那么您必须编写读取大端数字的代码。相反,您可以只使用big_endian int x;,编译器将为您生成代码。 - geza
我认为很难提出有说服力的理由来扩展核心语言以添加对非穷尽架构细节的显式支持。比较原子操作-选择两个最流行的内存顺序并忽略其他真正使用的顺序将破坏首先标准化它们的目的。 - Useless
1
如果C++撤回了他们不关心值的表示的选择,那么他们不仅应该定义字节序,还应该定义浮点数、有符号整数、结构体内存布局的表示方式...这是很多工作! - YSC
显示剩余5条评论

4
整数(作为数学概念)具有正数和负数的概念。这个抽象的符号概念在硬件上有许多不同的实现。
字节序不是一个数学概念。小端序是一种在具有16或32位寄存器和8位内存总线的微处理器上提高多字节二进制补码整数算术性能的硬件实现技巧。它的创造需要使用大端序来描述在寄存器和内存中具有相同字节顺序的其他所有内容。
C语言抽象机器包括有符号和无符号整数的概念,没有细节 - 不需要二进制补码算术、8位字节或如何将二进制数存储在内存中。
PS: 我同意在网络或内存/存储中的二进制数据兼容性是个麻烦事。

2
简短回答:如果不能在涉及整数的算术表达式中(没有重载运算符)使用对象,则这些对象不应该是整数类型。同时,在同一表达式中允许大端和小端整数相加和相乘没有意义。
更长回答:
正如有人提到的,字节序是处理器特定的。这实际上是指当数字作为机器语言中的地址和算术操作的操作数/结果时,数字是如何表示的。
同样的规则“有点”适用于符号。但程度不同。需要将从语言语义符号转换为处理器可接受符号,以便将数字用作数字。需要将大端和小端互换并反转以将数字用作数据(将它们发送到网络或表示有关发送到网络的数据的元数据,例如有效载荷长度)。
话虽如此,这种决策似乎主要是由用例驱动的。反面的是,忽略某些用例有一个好的实用理由。实用主义出自于字节序转换比大多数算术操作更昂贵。
如果一种语言具有将数字保留为小端的语义,它将允许开发人员通过强制程序中的数字为小端来让自己自食其果。如果在小端机器上开发,这种强制字节序的操作将不起作用。但当移植到大端机器时,会有很多意外的减速。如果相关变量既用于算术运算又用于网络数据,则会使代码完全不可移植。
没有这些字节序语义或强制它们明确地针对编译器,强制开发人员思考数字是如何“读取”或“写入”网络格式。这将使在算术操作中来回转换网络和主机字节顺序的代码笨重且不太可能成为懒惰开发者首选的编写方式。
由于开发是人类的努力,使错误的选择不舒服是一件好事。
编辑:以下是一个例子,说明这种情况可能会出现问题: 假设引入了little_endian_int32和big_endian_int32类型。那么little_endian_int32(7) % big_endian_int32(5)是一个常量表达式。它的结果是什么?数字是否会隐式转换为本地格式?如果不是,结果的类型是什么?更糟糕的是,结果的值是什么(在这种情况下,应该在每台机器上都相同)?
如果将多字节数字用作纯数据,则char数组同样适用。即使它们是“端口”(实际上是查找表或哈希表中的查找值),它们仅是字节序列而不是整数类型(可以进行算术运算)。现在,如果您将显式字节顺序数字上允许的算术操作限制为仅允许指针类型允许的操作,则可能会有更好的可预测性。然后,即使在大端机器上声明了类似于little_endian_int16的myPort,myPort + 5也是有意义的。lastPortInRange - firstPortInRange + 1也是一样。如果算术运算与指针类型的运算方式相同,则会按预期执行此操作,但firstPort * 10000将是非法的。然后,当然,您会进入是否通过任何可能的好处来证明功能膨胀的争论。

2

字节序并不是数据类型的本质属性,而是其存储布局的一部分。

因此,它与有符号/无符号并不完全相似,更像是结构体中的位域宽度。类似于这些,它们可以用于定义二进制API。

因此,您可以使用以下内容:

int ip : big 32;

这将定义存储布局和整数大小,然后由编译器来匹配对该字段的使用。我并不清楚它允许的声明有哪些。


2
这是一个很好的问题,我经常想到类似的东西会很有用。但是你需要记住,C语言旨在实现平台独立性,而字节序只有在这样的结构被转换为某种底层内存布局时才是重要的。例如,当您将uint8_t缓冲区转换为int时,就会发生这种转换。虽然字节序修饰符看起来很整洁,但程序员仍然需要考虑其他平台差异,比如int大小和结构对齐和打包。 为了防御性编程,当您想要对某些变量或结构在内存缓冲区中的表示方式进行更精细的控制时,最好编写显式转换函数,然后让编译器优化器为每个支持的平台生成最有效的代码。

这不是一个坏答案,提供了与我的观点互补的观点。但它可以稍微改进一下。 - YSC
1
当你将uint8_t缓冲区转换为int时,就会发生这种转换。仅进行强制转换是未定义的行为,因为它违反了别名规则;使用memcpy()是执行该转换的唯一明确定义的方法。然后,是的,结果是否有意义取决于源缓冲区和目标类型的相应字节布局。 - underscore_d

1

从实用程序员的角度来看,搜索 Stack Overflow,值得注意的是,这个问题的精神可以用一个实用库来回答。Boost 有这样一个库:

http://www.boost.org/doc/libs/1_65_1/libs/endian/doc/index.html

该库的特性与正在讨论的语言特性最相似的是一组算术类型,例如big_int16_t


0

因为没有人提议将其添加到标准中,和/或者因为编译器实现者从未感到需要。

也许你可以向委员会提出建议。我认为在编译器中实现它并不难:编译器已经提供了对目标机器不是基本类型的基本类型。

C++的发展是所有C++程序员的事情。

@Schimmel。不要听那些为现状辩护的人!所有为这个缺陷辩护的论点都非常脆弱。即使一个学生逻辑学家不知道计算机科学的任何东西,都可以发现它们的不一致性。只是提出建议,不要担心病态保守主义者。(建议:提出新类型而不是修饰符,因为unsignedsigned关键字被认为是错误)。


2
如果这样的提议成功了,那将是一个惊喜。字节序问题非常罕见,可以通过实用函数轻松解决。它们不一定需要在语言中存在。 - geza
1
将某些东西纳入C标准的唯一方法是指向现有编译器中成功实现的内容,因此开始的方法是让gcc/clang/其他人来实现它。然后联系委员会。 - pipe
@Oliv,你确定它没有被提出吗?我认为它会被拒绝,因为它将启用许多代码,这将使优化变得复杂。任何涉及计算的网络字节顺序整数的代码都需要进行大量转换或者使用优化器来删除这些转换。目前类型的方式使得编写混合主机字节和网络字节整数的代码非常麻烦。如果网络字节整数成为基本类型,那么它将变得太容易了。 - Dmitry Rubanovich
@Oliv,我知道好处。但问题不是“...的优点是什么?”而是“为什么它不存在?”所以自然要看缺点。该答案中的反转类将不会为常量表达式(在编译时计算)的整数模运算符“%”产生一致的结果。整数类型在进行算术运算时必须产生一致的结果。通过网络发送的任何内容都只是数据。它可能就是字节。 - Dmitry Rubanovich
@Oliv,这很奇怪,我已经多次惊叹于GCC优化的质量。如果编译器产生低质量的二进制文件,那么我们就完了:我们所有的推理和高级工具(标准容器、算法等)都严重依赖编译器将它们优化掉的能力。 - YSC
显示剩余12条评论

-1

字节序是编译器特定的,因为它是机器特定的,而不是平台无关性的支持机制。标准——是一个没有考虑强制实施使事情变得“容易”的规则的抽象——其任务是创建编译器之间的相似性,允许程序员为他们的代码创建“平台无关性”——如果他们选择这样做。

最初,平台之间存在很多市场份额竞争,同时编译器通常由微处理器制造商编写为专有工具,并支持特定硬件平台上的操作系统。英特尔可能并不太关心编写支持摩托罗拉微处理器的编译器。

C语言毕竟是由贝尔实验室发明用于重写Unix。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接