T*指针与char*指针的算术运算

11
假设我们有一个包含N个类型为T的元素的数组。
T a[N];

根据C++14标准,在什么条件下我们可以保证...
 (char*)(void*)&a[0] + n*sizeof(T) == (char*)(void*)&a[n],  (0<=n<N) ?

尽管这对于许多类型和实现是正确的,但标准在一个脚注中以一种含糊的方式提到了它:
§5.7.6,脚注85)处理指针算术的另一种方法...
很少有迹象表明这种其他方式被认为与标准方式等效。它可能更像是给实现者的提示,建议其中的一种符合实现。
编辑:
人们低估了这个问题的难度。
这个问题不是关于你可以从教科书中读到什么,而是关于你可以通过逻辑和推理从C++14标准中推断出什么。
如果使用“连续”或“连续地”,请说明正在连续的内容。
虽然T[]和T*密切相关,但它们是抽象的,T* x N上的加法可能以任何一致的方式由实现定义。
该方程式使用指针加法进行了重新排列。如果p指向char,则始终可以使用(§5.7(4))或一元加法来定义p + 1,因此我们不会遇到未定义行为。原始版本包括指针减法,这可能会导致早期的未定义行为。(char指针只进行比较,不进行解引用)。

3
这不应该总是成立吗? - Daniel Jour
1
我甚至没有被说服指针算术具有正式定义的行为。 - T.C.
1
请注意,指针算术在范围 0<=n<=N 是有效的,这同样适用于标量。 - Bathsheba
1
如果上面引用的段落适用于减法,那么同样适用于加法。 p + 1 被定义了,但这并不意味着 p + anything 也被定义了。公布的 C++14 标准文本被认为在这个领域有一些缺陷。一些已经被修复了(参见P0137),一些仍需要改进(例如CWG1701)。请记住,P0137澄清了 aa [0] 不是指针可互换的。这不仅涉及布局,还涉及为允许优化而施加的限制。 - bogdan
请记住,在 C 语言中,数组下标运算符是基于指针算术定义的:a[b] 等同于 *(a+b),这也是为什么它等同于 b[a] 的原因。在 C++ 中,由于操作符重载的存在,这个原则已经有所退化,但它仍然对任何 C 风格的数组布局施加了非常严格的限制。而这种限制保证了你的等式必须成立。 - cmaster - reinstate monica
显示剩余18条评论
3个回答

7

在 [dcl.array] 中:

数组类型的对象包含一组非空的、连续分配的 N 个类型为 T 的子对象。

连续意味着类型为 T 的任何相邻子对象之间的偏移量是 sizeof(T),这意味着第 n 个子对象的偏移量是 n*sizeof(T)

n < N 的上界来自于 [expr.add]:

当将整数类型的表达式添加到指针上或从指针中减去时,结果具有指针操作数的类型。如果表达式 P 指向数组对象 x 的元素 x[i](其中 x 具有 n 个元素),则表达式 P + JJ + P(其中 J 的值为 j)指向(可能是虚拟的)元素 x[i + j],如果 0 <= i + j < n;否则,行为未定义。


请参阅(§23.3.2.1)中对“连续”一词的定义,其中指出“连续”仅意味着“数组算术与指针算术兼容”。请参考http://stackoverflow.com/questions/39791806/array-of-non-contiguous-objects/39866710#39866710。 - Heiko Bloch
@HeikoBloch std::array 不是数组。 - Barry
但是这句话表明,单词“contiguously”(在分配或存储上相邻)和“contiguous”(在连续字节序列中)被用于略有不同的含义,这将有助于解决其他明显的矛盾。 - Heiko Bloch
1
@HeikoBloch “contiguously… and contiguous… are used with slightly different meanings” 这可能只能被视为标准中的一个缺陷。 - n. m.
@Barry:「最新的工作草案」和 C++14 不是同一回事。我相信我们之前已经讨论过这个问题了... - Lightness Races in Orbit
显示剩余8条评论

1
这是一个真实的规则,但与指针算术规则不同,您必须依赖于给定sizeof运算符(5.3.3 [expr.sizeof])的语义:
当应用于引用或引用类型时,结果是所引用类型的大小。当应用于类时,结果是该类对象中的字节数,包括放置该类型对象在数组中所需的任何填充。大多数派生类的大小必须大于零。将sizeof应用于基类子对象的结果是基类类型的大小。当应用于数组时,结果是数组中所有字节的总数。这意味着n个元素的数组的大小是n乘以一个元素的大小。
显然,只有一种打包方式可以将n个不重叠的元素放置在n * sizeof(element)的空间中,即它们是等间隔的sizeof (element)字节。并且只有一个排序是符合关系运算符部分下指针比较规则(5.9 [expr.rel])的要求的。
比较指向对象的指针定义如下:
  • 如果两个指针指向同一数组的不同元素或其子对象,则具有更高下标的元素的指针比较大。

你的意思是[dcl.array]中根本不需要连续性要求吗? - Heiko Bloch

-3

第一行的声明也是定义。(§3.1(2)) 它创建了数组对象。(§1.8(1))

由于别名规则,可以通过多个lvalue访问对象。(§3.10(10))特别地,右侧的对象可以通过char指针合法地访问(别名)。

让我们看一下数组定义中的一个句子,然后消除“连续”的歧义。

“数组类型的对象包含一组非空的N个T类型子对象,这些子对象是连续分配的。”[dcl.array] §8.3.4。


澄清

我们从二元对称关系“连续”开始,这适用于char对象,这应该是显而易见的。(“iff”代表“如果且仅当”,集合和序列是数学上的概念,不是C++容器)如果您能链接到更好或更被认可的定义,请评论。

字符对象的序列x_1…x_N是连续的,当且仅当对于所有i=1…N-1,x_i和x_{i+1}在内存中连续。

一组字符对象M是连续的,当且仅当可以为其中的对象编号,例如x_1…x_N,使得序列(x_i)_i连续。也就是说,如果M是一个连续的、单射序列的图像,那么它是连续的。

两个字符对象的集合M_1,M_2是连续的,当且仅当存在M_1中的x_1和M_2中的x_2是相邻的。

字符对象集合的序列M_1…M_N是连续的,当且仅当M_i和M_{i+1}对于所有i=1…N-1是连续的。

一组字符对象集合是连续的,当且仅当它是字符对象集合的连续单射序列的图像。

现在应该应用哪个版本的“连续”?语言重载解析:

1)“连续”可能指“分配”。由于分配函数调用提供了一组可用的char对象的子集,因此这将调用char对象集合的变体。也就是说,所有N个子对象中出现的所有char对象都应该是连续的。

2)“连续”可能指“集合”。这将调用char对象集合的集合变体,每个子对象都被视为char对象的集合。


这是什么意思?首先,虽然作者将数组子对象编号为a [0] ... a [N-1],但他们选择不说关于子对象在内存中的顺序:他们使用了“集合”而不是“序列”。他们描述了分配是连续的,但他们没有说a [j]和a [j + 1]在内存中是连续的。此外,他们选择不写下涉及(char *)指针和sizeof()的直接公式。虽然看起来他们故意将连续性与排序问题分开,但§5.9(3)要求所有类型的数组子对象具有相同的排序方式。
如果指针指向同一数组的两个不同元素或其子对象,则具有较高下标的元素的指针比较大。

现在,组成数组子对象的字节是否符合上述引用中的子对象的定义?通过阅读§1.8(2)和完整对象或子对象?,答案是:对于不包含子对象且不是字符数组的数组,至少目前来说不是。因此,我们可能会找到一些示例,其中没有对数组元素施加特定的排序。

但是,暂时假设我们的数组子对象仅由字符填充。考虑“连续”的两种可能解释意味着什么?

1)我们有一组连续的字节,与一组有序的子对象重合。那么OP中的声明是无条件成立的。

2)我们有一系列连续的子对象,每个子对象可能单独不连续。这可以通过两种方式实现:要么子对象可能存在间隙,即它们包含两个距离大于sizeof(subobject)-1的char对象。要么子对象可能分布在不同的连续字节序列中。

在情况2)中,不能保证OP中的声明成立。

因此,清楚“连续”是什么意思非常重要。

最后,这里有一个实现的例子,其中§5.9没有对数组子对象施加明显的排序,因为数组子对象本身没有子对象。读者提出了担忧,认为这将与其他地方的标准相矛盾,但尚未证明明确的矛盾。

假设T是int,并且我们有一个特定的符合要求的实现,以朴素的方式表现得像预期一样,只有一个例外:

它按相反的内存顺序分配int数组,将数组的第一个元素放在对象的高内存地址端:

a[N-1], a[N-2], ... a[0]  

替代

a[0], a[1],   ... a[N-1]  

这个实现满足任何合理的连续性要求,因此我们不必就“连续”达成单一解释就可以继续论证。

然后,如果 p 指向 a,将 p 映射到 &a[0](调用 [conv.array])会使指针跳转到 a 的高内存端附近。 由于数组算术必须与指针算术兼容,我们还需要

int * p= &intVariable;
(char*)(p+1) + sizeof(int) == (char*)p

int a[N];

(char*)(void*)&a[n] + n*sizeof(int)==(char*)(void*)&a[0],  (0<=n<N)

然后,对于T=int,无法保证原帖中的声明是真实的。


编辑历史:删除并以修改后的形式重新引入了可能存在错误的快捷方式,这是由于未应用指针<关系规范的相关部分所致。尚未确定是否有正当理由,但关于连续性的主要论点仍然成立。


1
嗯,数组的索引被定义为加法。你怎么想到可以向后走呢? - Barry
他们将分配描述为连续的,但他们并没有说a[j]和a[j+1]在内存中是连续的。你认为在这种情况下连续意味着什么?你的解释显然是它纯粹是装饰性的。 - Barry
@BenVoigt 按照哪种指针类型进行升序排序?在我的示例中,数组元素按照 int* 指针进行升序排列,但按照 char* 指针进行降序排列。没有人说指针强制转换必须单调递增。在这个示例中,(char*) 强制转换是一个关于相应有序关系的递减函数。如果您发现这是不允许的,请告诉我原因。 - Heiko Bloch
@Heiko:我已经这样做了。规则同样适用于指向数组的char*int* - Ben Voigt
4.2节表示:“类型为'N个T的数组'或'未知边界的T数组'的左值或右值可以转换为类型为'T指针'的prvalue。结果是数组的第一个元素的指针。”请解释一下您的降序系统如何处理未知边界的数组。不要忘记,所有具有相同元素类型的数组都必须使用相同的排序方案,与'int *'使用的相同方案相同。 - Ben Voigt
显示剩余7条评论

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