【问题标题】:Confusion regarding types, overflows and UB in pointer-integral addition关于指针积分加法中的类型、溢出和 UB 的混淆
【发布时间】:2015-06-12 13:44:04
【问题描述】:

我曾经认为向指针添加整数类型(假设指针指向特定大小的数组等)总是很好定义的,无论整数类型如何。 C++11 标准说 ([expr.add]):

当一个整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果和原始数组元素的下标之差等于积分表达式。换句话说,如果表达式 P 指向数组对象的第 i 个元素,则表达式 (P)+N(等效于 N+(P))和 (P)-N(其中 N 的值为 n)指向分别指向数组对象的第 i + n 个和第 i - n 个元素,前提是它们存在。此外,如果表达式 P 指向数组对象的最后一个元素,则表达式 (P)+1 指向数组对象的最后一个元素,如果表达式 Q 指向数组对象的最后一个元素,则表达式 (Q)-1 指向数组对象的最后一个元素。如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

另一方面,最近引起我注意的是,指针的内置加法运算符是根据ptrdiff_t 定义的,这是一个有符号类型(参见 13.6/13)。这似乎暗示如果一个人使用非常大(无符号)大小的malloc(),然后尝试通过带有std::size_t 值的指针添加来到达分配空间的末尾,这可能会导致未定义的行为,因为未签名的std::size_t 将转换为ptrdiff_t,这可能是UB。

我想类似的问题会出现,例如,在std::vectoroperator[]() 中,它是根据未签名的size_type 实现的。一般来说,在我看来,这样几乎不可能充分利用平台上的可用内存。

值得注意的是,在将无符号值添加到指针时,GCC 和 Clang 都没有抱怨有符号-无符号整数转换,所有相关诊断都打开了。

我错过了什么吗?

编辑:我想澄清一下,我说的是涉及指针和整数类型(不是两个指针)的加法。

EDIT2:提出问题的等效方法可能是这样。如果ptrdiff_t 的正数范围小于size_t,此代码是否会导致第二行出现UB?

char *ptr = static_cast<char * >(std::malloc(std::numeric_limits<std::size_t>::max()));
auto end = ptr + std::numeric_limits<std::size_t>::max();

【问题讨论】:

  • 那么在 13.6 中为指针定义的运算符是什么?我说的是积分+指针,而不是指针+指针。
  • 好的,也许你应该澄清一下。
  • 标题说“指针积分”添加,但为了清楚起见,我会编辑帖子。
  • 如果委员会/Stroustroup 一开始没有为此使用无符号类型就好了。
  • @BaummitAugen:Bjarne 或 C++ 委员会在这方面的唯一部分是决定 C++ 在这一点上应该与 C 保持兼容。 C 委员会已经决定(例如)sizeof 将产生无符号类型的结果,早在 C++ 标准存在之前。

标签: c++ pointers c++11


【解决方案1】:

您的问题基于错误的前提。

指针相减产生 ptrdiff_t §[expr.add]/6:

当两个指向同一个数组对象的元素的指针相减时,结果是两个数组元素的下标之差。结果的类型是实现定义的有符号整数类型;此类型应与标头 (18.2) 中定义为 std::ptrdiff_t 的类型相同。

不过,不是意味着加法是根据ptrdiff_t 定义的。相反,对于加法,只指定了一次转换(§[expr.add]/1):

对算术或枚举类型的操作数执行通常的算术转换。

“通常的算术转换”在 §[expr]/10 中定义。这仅包括从无符号类型到有符号类型的一次转换:

否则,如果带符号整数类型的操作数的类型可以表示无符号整数类型的操作数类型的所有值,则将无符号整数类型的操作数转换为带符号整数类型的操作数的类型输入。

因此,虽然对于 size_t 将被转换为什么类型(以及它是否完全被转换)可能存在一些疑问,但有一点是毫无疑问的:它可以转换为的唯一方法ptrdiff_t 是如果它的所有值都可以不加更改地表示为 ptrdiff_t

所以,给定:

size_t N;
T *p;

...表达式p + N永远失败,因为在添加之前将N 某些(想象的)转换为ptrdiff_t

既然提到了 §13.6,也许最好备份并仔细查看 §13.6 的真正含义:

代表第 5 章中定义的内置运算符的候选运算符函数在本节中指定。这些候选函数参与 13.3.1.2 中所述的运算符重载决策过程,并且不用于其他目的

[强调添加]

换句话说,§13.6 定义了一个将ptrdiff_t 添加到指针的运算符这一事实意味着当任何其他整数类型添加到指针时,它首先转换为一个ptrdiff_t,或类似的东西。更一般地说,第 13.6 节中定义的运算符从不用于执行任何算术运算

有了这个,以及您从 §[expr.add] 引用的其余文本,我们可以很快得出结论,当且仅当指针后的数组。

鉴于上述情况,您可能又想到了一个问题。如果我有这样的代码:

char *p = huge_array;
size_t N = sizeof(huge_array);
char *p2 = p + N;

ptrdiff_t diff = p2 - p;

...最后的减法会不会溢出?简短的回答是:是的,可以。

【讨论】:

  • 谢谢!所以 13.6 中的操作符只用于重载目的,而从未实际实例化?
  • @bluescarni:正确。他们从来不习惯做实际的加法,或者按这个顺序做任何事情。
猜你喜欢
  • 2023-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-15
  • 1970-01-01
相关资源
最近更新 更多