【问题标题】:Pointer interconvertibility vs having the same address指针互转换与具有相同地址
【发布时间】:2018-06-04 01:55:33
【问题描述】:

working draft of the standard N4659 说:

[basic.compound]
如果两个对象是指针可互转换的,那么它们的地址相同

然后注意到

一个数组对象和它的第一个元素不是指针互转换的,即使它们有相同的地址

使数组对象及其第一个元素不可指针互转换的基本原理是什么?更一般地说,将指针互转换的概念与具有相同地址的概念区分开来的基本原理是什么?是不是哪里有矛盾?

看起来,给定这个语句序列

int a[10];

void* p1 = static_cast<void*>(&a[0]);
void* p2 = static_cast<void*>(&a);

int* i1 = static_cast<int*>(p1);
int* i2 = static_cast<int*>(p2);

我们有 p1 == p2,但是,i1 定义明确,使用 i2 会导致 UB。

【问题讨论】:

  • @MartinBonner Thisthis
  • @Someprogrammerdude 数组不是指针,但数组的第一个元素也不是指针(通常)。我“指针可互转换”是关于标准化何时可以通过静态转换在基指针和派生指针之间转换为void* 并返回(以及何时不能)。
  • 我发现这一段关于static_cast(void*) 也是obscurantist [expr.static.cast]: "。否则,如果原来的指针值指向一个对象a,并且有一个类型为 T 的对象 b(忽略 cv 限定),它可以与 a 进行指针互转换(6.9.2),结果是指向 b 的指针。否则,指针值不会因转换而改变。”。如果指针值指向正确的地址,它是一个有效的指针值吗?
  • 如果您阅读下面的 cmets |this question](stackoverflow.com/questions/47616508/…),您会了解到 C++ 内存模型已经并且仍然主要受 Boehm 的影响,Boehm 出售垃圾收集器库。自从我阅读此评论后,我怀疑 C++ 内存模型中的不一致是由于其兴趣的影响,而不是出于理性原因。
  • @xskxzr 如果数组是结构的第一个成员并且我们有一个指向数组第一个元素的指针并且想要访问该结构怎么办?

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


【解决方案1】:

显然存在基于此优化的现有实现。考虑:

struct A {
    double x[4];
    int n;
};

void g(double* p);

int f() {
    A a { {}, 42 };
    g(&a.x[1]);
    return a.n; // optimized to return 42;
                // valid only if you can't validly obtain &a.n from &a.x[1]
}

给定p = &amp;a.x[1];g 可能会尝试通过reinterpret_cast&lt;A*&gt;(reinterpret_cast&lt;double(*)[4]&gt;(p - 1))-&gt;n 获取对a.n 的访问权限。如果内部转换成功产生了一个指向a.x 的指针,那么外部转换将产生一个指向a 的指针,从而为类成员访问定义行为,从而禁止优化。

【讨论】:

  • @T.C.我猜它会优化,因为标准给出了优化的明确许可,而不是相反。如果 x 不是数组,它将无法优化。我的问题是,为什么标准在数组和非数组之间画一条线?这似乎完全是武断的。
  • 有趣。这是什么编译器?我在 godbolt.org 上没有找到。
  • @M.M 仅仅因为它代表相同的地址(“指向相同的字节”)并不意味着它在抽象机中具有相同的指针值。内部转换的结果是一个类型为“指向 4 double 数组的指针”的指针,其值为“指向a.x 的第一个元素的指针”,因此外部转换的结果是一个“指针”类型的指针到A”,其值为“指向a.x 的第一个元素的指针”,并且由于它实际上并不指向A 对象,因此当类成员访问表达式用于访问非A 的静态数据成员。
  • [basic.lval]/11 意义上的“严格别名”是无关紧要的。 reinterpret_cast&lt;A*&gt;(reinterpret_cast&lt;double(*)[4]&gt;(p - 1))-&gt;n 不执行该规则含义内的“访问”。至于你的替代表达式,任何地方都没有char数组,指针算法是未定义的。
  • @T.C.根据您的最后评论,是否可以说核心原理(这个问题所问的)是指向数组元素的指针不能“转义”它们的数组。与void h(double(*)[4]); h(&amp;a.x); 相比 - 可能不再可能进行优化,因为h 可能会将其参数转换为A *;该强制转换是正确的,因为指向标准布局结构的指针可以与其第一个元素的指针相互转换。
【解决方案2】:

更一般地说,区分指针互转换性的概念和具有相同地址的概念的基本原理是什么?

很难回答为什么某些决定是由标准做出的,但这是我的看法。

从逻辑上讲,指针指向对象,而不是地址。地址是指针的值表示。在重用包含const 成员的对象的空间时,这种区别尤其重要

struct S {
    const int i;
};

S s = {42};
auto ps = &s;
new (ps) S{420};
foo(ps->i);  // UB, requires std::launder

具有相同值表示的指针可以像使用相同的指针一样使用,应该将其视为特殊情况,而不是相反。

实际上,该标准试图对实现施加尽可能少的限制。 Pointer-interconvertibility 是指针可能是reinterpret_cast 并产生正确结果的条件。看到reinterpret_cast 是如何被编译为空的,这也意味着指针共享相同的值表示。由于这对实现施加了更多限制,因此在没有令人信服的理由的情况下不会给出条件。

【讨论】:

  • 我不太明白这与手头的问题有什么关系。当两个不同的对象在不同的​​时间驻留在同一个地址时,使用指向一个的指针就好像它是指向另一个的指针与一些合理的优化冲突。但是我们有一个数组及其元素,即一个对象及其位于对象开头的子对象。为什么这适用于结构及其第一个成员,但不适用于数组及其第一个元素?这里的概念区别是什么?如果数组也允许这样做会带来什么困难?
  • @n.m.我的观点是可能没有什么基本的东西。仅当出现令人信服的原因时才给出指针互转换性,因为它在逻辑上很奇怪(破坏抽象),并且可能会限制实现。不是没有理由不这样做,而是没有理由这样做。
  • 我不明白适用于结构子对象的推理如何也不适用于数组子对象以实现指针可转换性。可能有一些东西适用于一个而不是另一个,但我不明白它是什么。
  • @n.m.好吧,C 支持结构子对象,而 C++ 中的遗留 C 代码假定它可以工作。
  • 如果有一个理由文件解释为什么所有这些规则都存在,那就太好了
【解决方案3】:

因为委员会想要明确数组是一个低级概念,而不是一等对象:例如,您不能返回数组或分配给它。指针互转换是同一级别对象之间的概念:仅标准布局类或联合。

这个概念在整个草案中很少使用:在 [expr.static.cast] 中,它作为一种特殊情况出现,在 [class.mem] 中,注释说对于标准布局类,指针对象及其第一个子对象是可相互转换的,在 [class.union] 中,指向联合及其非静态数据成员的指针也被声明为可相互转换的,并且在 [ptr.launder] 中。

最后一次出现将两个用例分开:指针是可相互转换的,或者一个元素是一个数组。这在备注中说明,而不是在 [basic.compound] 中的注释中说明,因此更清楚地表明 pointer-interconvertibility 自愿不涉及数组。

【讨论】:

  • "允许实现对对象指针和数组指针有不同的表示" 那么为什么指向数组的指针可以转换为指向其结构 super 对象的指针呢? (当然是第一个成员)
  • @n.m.:经过一番思考,我认为指向数组的指针不能转换为指向其超级对象的指针,因为数组不是标准布局对象。我的观点是数组的第一个元素是可转换为超级对象的指针,但不能转换为数组。当然,当转换为字节指针(C++17 之前的 char 指针)时,所有 3 都将转换为相同的指针,因为数组的第一个字节是其第一个元素的第一个字节。然后可以将指向数组的指针转换为指向其第一个元素的指针(通过 char* 或 reinterpret_cast),...
  • ... 但没有什么能保证指向数组的指针和指向对象(第一个元素和超级对象)的指针具有相同的表示形式。指向对象的指针可以静态转换为指向子类的指针并返回,但子类对象可以位于不同的地址。
  • 一个数组和它的第一个元素有相同的地址。它是由标准保证的。没有人在谈论数组第一个元素的子类对象。仅关于第一个元素本身。保证数组可以转换为其标准布局 super 对象,因此这些指针必须具有相同的表示形式,或者表示形式的差异无关紧要。
  • 此外,struct {int i;} 及其第一个元素是指针可转换的,即使相应的指针被非常明确地允许具有不同的表示形式。
【解决方案4】:

仔细阅读标准的这一部分后,我了解到两个对象是指针互转换的,顾名思义,如果

  1. 它们是“相互连接的”,通过它们的类定义(请注意,指针可互换概念是为类对象及其第一个非静态数据成员定义的)。

  2. 它们指向同一个地址。但是,由于它们的类型不同,我们需要使用reinterpret_cast 运算符“转换”它们的指针类型。

对于问题中提到的数组对象,数组和它的第一个元素在类定义方面没有互连,我们也不需要转换他们的指针类型能够与他们一起工作。它们只是指向同一个地址。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-01
    • 1970-01-01
    相关资源
    最近更新 更多