【问题标题】:T* versus char* pointer arithmeticT* 与 char* 指针运算
【发布时间】:2017-02-14 03:36:57
【问题描述】:

假设我们有一个包含 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 标准中推导出什么。

如果您使用“contiguous”或“contiguously”,请同时说明什么是连续的。

虽然 T[] 和 T* 密切相关,但它们是抽象,在 T* x N 上的加法可以由实现以任何一致的方式定义。

使用指针加法重新排列方程。如果 p 指向一个字符,则 p+1 总是使用 (§5.7 (4)) 或一元加法来定义,所以我们不会遇到 UB。原始包含一个指针减法,这可能在早期就导致了 UB。 (char 指针只进行比较,不取消引用)。

【问题讨论】:

  • 这不应该永远是真的吗?
  • 我什至不相信指针算法已经正式定义了行为。
  • 请注意,指针运算在0&lt;=n&lt;=N 范围内有效,这也适用于标量。
  • 在已发布的 C++14 标准中,[expr.add]/7 没有回答您的问题吗?据我所知,答案是,在任何情况下,很遗憾。在当前的草稿中,这是paragraph 6(稍微放松,但仍然不允许减法)。
  • 如果上面引用的段落适用于减法,它也适用于加法。 p + 1 已定义,但这并不意味着 p + anything 也已定义。已发布的 C++14 标准文本被认为在这方面存在一些缺陷。有些已经修复(参见P0137),有些仍然需要工作(例如,CWG1701)。请记住,P0137 澄清了 aa[0] 不是指针可互转换的。这不仅与布局有关,还与为优化而施加的限制有关。

标签: c++ arrays pointers c++14 language-lawyer


【解决方案1】:

在 [dcl.array] 中:

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

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

n &lt; N的上限来自[expr.add]:

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

【讨论】:

  • 参见 (§23.3.2.1) 中的 contiguously 的定义,它表示“contiguously”仅表示“数组算术与指针算术兼容”。比较stackoverflow.com/questions/39791806/…
  • @HeikoBloch 在最新的工作草案中没有这样的定义。
  • 我指的是这句话:'数组的元素是连续存储的,这意味着,......'。虽然它不是正式的定义,但它是最接近的句子。
  • @HeikoBloch std::array 不是数组。
  • @HeikoBloch "contiguously... 和 contiguous... 使用的含义略有不同" 这只能被视为标准中的缺陷。
【解决方案2】:

这总是正确的,但是您必须依赖为 sizeof 运算符 (5.3.3 [expr.sizeof]) 提供的语义,而不是查看指针运算规则:

当应用于引用或引用类型时,结果是被引用类型的大小。 当应用于一个类时,结果是该类的对象中的字节数,包括将该类型的对象放入数组中所需的任何填充。 最派生类的大小应更大比零。 将 sizeof 应用于基类子对象的结果是基类类型的大小。 当应用于数组时,结果是数组中的总字节数。这意味着 n 个元素的数组的大小是元素大小的 n 倍。

应该清楚的是,只有一种包装将 n 个不重叠的元素放在n * sizeof(element) 的空间中,即它们定期间隔sizeof (element) 字节。并且在关系运算符部分 (5.9 [expr.rel]) 下找到的指针比较规则只允许一种排序:

比较指向对象的指针定义如下:

  • 如果两个指针指向同一数组的不同元素或其子对象,则指向具有较高下标的元素的指针比较大。

【讨论】:

  • 你的意思是根本不需要[dcl.array]中的连续性要求吗?
【解决方案3】:

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

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

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

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


消歧

我们从 char 对象的二进制对称关系“连续”开始,这应该是显而易见的。 ('iff' 是 'if and only if' 的缩写,集合和序列是数学的,而不是 C++ 容器)如果可以 链接到更好或更公认的定义、评论。

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

一组 M char 对象是连续的,当且仅当 M 可以被编号,例如 x_1 ...x_N,使得序列 (x_i)_i 是连续的。 也就是说,当且仅当 M 是一个连续的单射序列的图像。

两组 M_1、M_2 的 char 对象是连续的,如果有的话 在 M_1 中存在 x_1,在 M_2 中存在 x_2,使得 x_1 和 x_2 是连续的。

一个序列 M_1 ... M_N 的 char 对象集是连续的,如果 M_i 和 M_{i+1} 对于所有 i=1...N-1 都是连续的。

一组 char 对象是连续的,当且仅当它是 char 对象集的连续单射序列。

现在要应用哪个版本的“连续”?语言过载解决方案:

1) 'contiguous' 可能指的是'allocation'。作为分配函数调用提供了一个 可用 char 对象的子集,这将调用 set-of-chars 变体。那是, 出现在任何 N 个子对象中的所有 char 对象的集合将意味着是连续的。

2) 'contiguous' 可能指的是 'set'。这将调用 set-of-sets-of-chars 变体,每个子对象都被视为一组 char 对象。


这是什么意思?首先,虽然作者将数组子对象编号为 a[0] ... a[N-1],但他们选择不提及 内存中子对象的顺序:他们使用“set”而不是“sequence”。 他们将分配描述为连续的,但他们没有说 a[j] 和 a[j+1] 在内存中是连续的。此外,他们选择不写下 涉及 (char*) 指针和 sizeof() 的简单公式。虽然看起来他们 故意将连续性与排序关注点分开, §5.9 (3) 要求对所有类型的数组子对象进行相同的排序。

如果指针指向同一数组的两个不同元素,或其子对象,则指针 下标越高的元素比较越大。

现在组成数组子对象的字节是否符合条件 上述引用意义上的子对象?阅读§1.8(2) 和Complete object or subobject? 答案是:不,至少对于其元素不包含子对象且没有字符数组的数组来说不是这样,例如整数数组。因此,我们可能会发现没有对数组元素施加特定排序的示例。

但现在让我们假设我们的数组子对象只填充了字符。 考虑到“连续”的两种可能解释,这意味着什么?

1) 我们有一组连续的字节与一组有序的子对象一致。 那么 OP 中的声明是无条件正确的。

2) 我们有一个连续的子对象序列,每个子对象可能单独不连续。 这可能以两种方式发生:要么子对象可能有间隙,也就是说,它们 包含两个距离大于 sizeof(subobject)-1 的 char 对象。或者 子对象可能分布在不同的连续字节序列中。

在情况 2) 中,不能保证 OP 中的声明是正确的。

因此,明确“连续”的含义很重要。


最后,这是一个实现示例,其中第 5.9 节没有对数组子对象强加明显的顺序,因为数组子对象本身没有子对象。读者担心这会与其他地方的标准相矛盾,但尚未证明有明确的矛盾。

假设 T 是 int,并且我们有一个特定的符合要求的实现,它的行为与预期一样天真,但有一个例外:

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

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

而不是

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

此实现满足任何合理的连续性 要求,所以我们不必就单一的解释达成一致 'contiguous' 继续论证。

那么如果 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,不能保证原帖中的说法是正确的。


编辑历史:删除并以修改后的形式重新引入一个可能错误的快捷方式,该快捷方式是由于未应用指针

【讨论】:

  • 呃,对数组的索引被定义为加法。你怎么认为你可以倒退?
  • “他们将分配描述为连续的,但没有说 a[j] 和 a[j+1] 在内存中是连续的。”在这种情况下,您认为连续意味着什么?您的解释显然是纯粹的装饰性的。
  • @Barry 回答了你的问题。轮到你了。
  • “他们选择不说内存中子对象的顺序”和“标准中没有任何内容阻止实现以相反的内存顺序分配整数数组”是完全错误的,部分5.9 肯定要求元素按升序排列。
  • @BenVoigt 升序相对于哪种指针类型?在我的示例中,数组元素相对于 int* 指针按升序放置,但相对于 char* 指针按降序放置。没有人说指针转换必须单调递增。在该示例中, (char*) 强制转换是相对于各个排序关系的递减函数。如果您发现这是不允许的,请告诉我在哪里。
猜你喜欢
  • 1970-01-01
  • 2018-04-05
  • 1970-01-01
  • 2021-11-26
  • 1970-01-01
  • 2018-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多