← 返回信息流
AI 资讯Hacker News·3 小时前

AI生成内容几乎总是匿名发布

原标题:Almost Always Unsigned

速览

一项最新调查显示,由AI生成的文本、图像等内容在网络上发布时,几乎从未附带来源或作者签名。这引发了关于版权归属和内容可信度的讨论。研究者呼吁平台和用户增加透明度。

AI 深度解读

背景

在编程实践中,整数类型的选择长期存在争议。大多数整数在程序中永远不会表示负值(例如数组索引、循环计数),按理说无符号整数应该比有符号整数更常用。然而,在实际代码中,开发者几乎总是默认选择有符号整数。这篇文章由 Dale Weiler 撰写,发布于 Hacker News,主要针对 C 和 C++ 语言,同时也涉及 Go、Rust 和 Odin 等语言,试图论证这一原则适用于所有语言——无论语言对整型溢出的处理方式如何(例如 C/C++ 将有符号整数溢出定义为未定义行为),但问题本质上是算术运算本身的特性。

核心内容

反对无符号整数的常见论点及其反驳

安全性论点

最常见的反对理由是:无符号整数更容易发生下溢(underflow),比上溢更危险。这一建议如此普遍,以至于 Google 官方 C++ 编码规范明确反对使用无符号类型。然而,作者认为这些安全性问题源于不当的使用习惯,通过简单惯用法可以轻松避免,比处处使用有符号整数更容易理解。实际上,坚持使用有符号整数反而会助长不安全代码的编写。

反向循环论点

当 for 循环需要反向计数,且循环体在计数器为零时仍需执行时,多数程序员认为无符号整数难以使用,因为 i >= 0 永远为真。常见的做法是强制转换成有符号整数:

for (int64_t i = (int64_t)size - 1; i >= 0; i--) { ... }

这很危险:它涉及窄化转换,强制转换会压制合法警告。在 C/C++ 中,当 size 为特定大值时会触发未定义行为(例如 size >= 0x7fffffffffffffff 时,强制转换后的 i 可能变成负值,导致越界访问)。反对者常辩称这种输入是“病态的”,但作者指出这种说法不仅错误,而且更危险——因为 int64_t 根本不允许大于 0x7fffffffffffffff 的值,只是回避了问题。如果你真的需要那么大值,则必然触发有符号溢出(未定义行为),后果更严重。

正确的做法是利用无符号整数的回绕(wrap)特性(C/C++ 中无符号下溢是明确定义的):

for (size_t i = size - 1; i < size; i--) {
    // ...
}

从 size-1 开始,每次递减。当计数器为零时,递减操作使计数器下溢回绕到无符号类型的最大值,该值远大于 size,因此条件 i < size 为假,循环终止。即使 size == 0,0 - 1 产生最大值 > 0,循环不会进入。

在 Rust 中,Debug 构建下无符号下溢会触发 panic,但可以用 Range 特性安全实现:

for i in (0..size).rev() {
    // ...
}

无需强制转换,无隐蔽错误,所有合法输入均正常工作,覆盖 [0, 0xffffffffffffffff) 整个范围。

两个数的差可能为负数

当需要计算两个数的差(或绝对值差)时,常写成 delta = x - y;delta = abs(x - y);。反对者认为无符号不安全,因为若 y > x 则下溢。但作者指出,这个论证无效,因为无论 x、y 有无符号,代码本身就有问题:对于有符号整数,同样存在可能的下溢(比如 x 为负或 y 为正时),在 C/C++ 中同样触发未定义行为。而且即使是支持回绕的语言,INT_MAX - INT_MIN 的结果也是错误的。安全计算差值对有符号整数而言极其困难,例如:

if ((y > 0 && x < INT_MIN + y) || (y < 0 && x > INT_MAX + y)) {
    // error
} else {
    delta = abs(x - y);
}

而对无符号整数,只需:

delta = max(x, y) - min(x, y);

总是安全给出绝对值差,表达式自文档化,可读性也更好。

用有符号计算索引更安全

反对者声称,对于更复杂的索引表达式(如区间中点),使用有符号更安全。作者以二分查找中常见的中值计算为例:

int mid = (low + high) / 2;

当 low + high 超过 2^31-1 时,有符号溢出为负值,结果错误。即使改用更大的有符号类型(如 int64_t)也无法避免,因为仍可能超过 2^63-1。事实上,在任何语言中,安全计算两个变量的中点都几乎不可能——即使考虑回绕,也存在特定输入导致失败。常见改进写法 int mid = low + (high - low) / 2;high = INT_MAX, low = INT_MIN 时仍然失败。

若使用无符号整数,直接写 size_t mid = (low + high) / 2;low = 0x80000000, high = 0x80000002 时也会下溢得到错误结果 1(正确值 0x80000001)。但是,当这些值用作数组索引时,有符号行为几乎肯定会产生无效索引,导致内存安全问题;而无符号版本虽然结果可能不是精确中点,但索引始终在数组范围内(不会越界)。

关键要点

  • 无符号整数是默认选择:大多数整数从不表示负值(数组索引、循环计数),因此应优先使用无符号类型,而非有符号。
  • 安全性的真正来源是用法而非类型:所谓无符号易下溢的论点忽略了有符号溢出更危险(未定义行为,可能导致安全漏洞)。正确惯用法(如反向循环的 i < size 模式)简单且安全。
  • 反向循环的优雅解法:利用无符号回绕特性,用递减后与 size 比较代替 i >= 0,可安全处理包括 size=0 在内的所有情况。Rust 等语言可通过 .rev() 方法直接实现。
  • 绝对值差计算:对无符号整数,max(x,y) - min(x,y) 总是安全且自解释;对有符号整数则需复杂检查。
  • 中点计算并非无符号的弱点:无论有无符号,安全计算中点都困难。但作为数组索引时,无符号结果不会越界,有符号则可能导致内存安全问题。
  • 语言差异:C/C++ 无符号回绕是明确定义的;Rust Debug 模式会 panic,但用 Range 可安全实现相同行为;Go/Odin 有符号也回绕,但可能导致逻辑错误。
  • 类型转换的危险:强制将无符号转为有符号(如 int64_t i = (int64_t)size - 1)引入窄化转换和未定义行为,应避免。

意义与影响

这篇文章挑战了业界长期存在的“默认使用有符号整数”的惯例(尤其是受 Google C++ 编码规范影响)。它指出,许多反对无符号的理由源于对下溢/上溢行为的误解,以及对未定义行为的忽视。作者通过具体代码示例证明,采用无符号整数配合简单惯用法,不仅更安全(避免未定义行为),还能覆盖更大的数值范围,同时保持代码清晰。这一观点对系统编程(C/C++)、并发语言(Go/Rust)等场景具有实际指导意义:开发者应重新审视类型选择,认识到有符号整数的溢出风险远大于无符号的下溢风险,尤其是在涉及内存安全时。文章也提醒语言设计者,明确定义的算术行为(如 Rust 的 Debug 检查 + Release 回绕)比模糊的未定义行为更有利于编写健壮代码。

查看原文 →graphitemaster.github.io