指针可互换性与具有相同地址的区别

33

标准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 将导致未定义行为。


2
@MartinBonner 这个这个 - Passer By
2
@Someprogrammerdude 数组不是指针,但通常情况下数组的第一个元素也不是指针。我猜测,“指针可互换”是关于标准化何时可以通过静态转换将基类和派生类指针转换为void *并返回(以及何时不能)。 - Martin Bonner supports Monica
1
我认为这段关于static_cast<T*>(void*)的解释看起来比较晦涩不清,具体在[expr.static.cast]中提到:除非原始指针值指向一个对象a,并且存在类型为T(忽略cv-qualification)的对象b与a互相可转换(6.9.2),否则指针值通过转换不会改变。如果指针指向正确的地址,那么指针值是否有效? - Oliv
2
如果你阅读了下面的评论,你会发现C++内存模型在很大程度上受到了Boehm的影响,他销售垃圾回收器库。自从我读到这个评论以来,我怀疑C++内存模型的不一致性是由于其利益的影响而非合理的原因所导致的。 - Oliv
1
如果数组是结构体的第一个成员,我们有一个指向数组第一个元素的指针,并且想要访问该结构体,该怎么办? - n. m.
显示剩余12条评论
4个回答

28

显然现有的实现是基于此进行优化的。考虑以下内容:

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 = &a.x[1];g 可能会尝试通过 reinterpret_cast<A*>(reinterpret_cast<double(*)[4]>(p - 1))->n 访问 a.n。如果内部转换成功地生成了指向 a.x 的指针,则外部转换将生成指向 a 的指针,从而给出类成员访问定义行为,并因此禁止优化。


3
有趣。这是哪个编译器?我在godbolt.org上没有找到任何信息。 - n. m.
3
尽管它表示相同的地址(“指向相同的字节”),但这并不意味着在抽象机器中它具有相同的指针值。内部转换的结果是一个类型为“指向4个double数组的指针”的指针,其值为“指向a.x的第一个元素的指针”,因此外部转换的结果是一个类型为“指向A的指针”的指针,其值为“指向a.x的第一个元素的指针”。由于它实际上并没有指向一个A对象,所以当类成员访问表达式用于访问A的非静态数据成员时,它具有未定义的行为。 - T.C.
2
在[basic.lval]/11中,“严格别名”是无关紧要的。reinterpret_cast<A*>(reinterpret_cast<double(*)[4]>(p - 1))->n不符合该规则中“访问”的含义。至于您的替代表达式,由于没有任何char数组,指针算术是未定义的。 - T.C.
1
@M.M请参阅核心问题1701。这部分从未在标准中得到适当的规定,因此评估它在清理后的指针模型中的工作方式并没有真正意义。当它被规定时,可能需要将允许的指针算术范围限制为通过原始指针可访问的内存(就像launder),以允许所涉及的优化。 - T.C.
3
基于你最近的评论,可以这么说核心理由(本问题所询问的)是为了防止指向数组元素的指针“逸出”其数组范围。与 void h(double(*)[4]); h(&a.x); 相比较——很可能优化不再可行,因为 h 可能会将其参数转换为 A *;这个转换是正确的,因为标准布局结构体的指针可以与指向其第一个元素的指针相互转换。 - M.M
显示剩余12条评论

3
更一般地说,将指针可互相转化的概念与具有相同地址的概念区分开来的理由是什么?
很难甚至不可能回答标准为何做出某些决定,但这是我的看法。
逻辑上,指针指向对象,而不是地址。地址是指针的值表示。当重用包含const成员的对象的空间时,这种区别尤其重要。
struct S {
    const int i;
};

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

应该将具有相同值表示的指针用作相同指针视为特例,而不是反过来。

实际上,标准试图在实现上尽可能少地施加限制。指针可互换的条件是指针可以进行reinterpret_cast并产生正确的结果。由于reinterpret_cast意味着编译成无操作,这也意味着指针共享相同的值表示。由于这会对实现施加更多限制,因此只有在有强制性原因时才会给出该条件。


1
我不太明白这与手头的问题有什么关系。当两个不同的对象在不同的时间驻留在同一地址时,使用一个指向其中一个对象的指针作为指向另一个对象的指针会与一些合理的优化冲突。但是我们有一个数组及其元素,即一个对象及其子对象位于对象的开头。为什么结构体及其第一个成员可以这样做,而数组及其第一个元素却不行?这里的概念差异是什么?如果也允许数组这样做,会遇到什么困难? - n. m.
2
我不明白适用于结构体子对象的推理在指针可转换性方面为什么不适用于数组子对象。肯定有某些情况适用于其中一种但不适用于另一种,但我却不知道是什么。 - n. m.
@n.m. 嗯,C支持结构体子对象,并且在C++中的遗留C代码假定它可以工作。 - Yakk - Adam Nevraumont
4
如果有一份解释为什么所有这些规则存在的理由文档,那就太好了。 - M.M
“从逻辑上讲,指针指向对象而不是地址。” 这与“指针具有平凡类型”相矛盾,但没关系。 - curiousguy
显示剩余7条评论

2
因为委员会想要明确一个数组是低级概念而不是一流对象:例如,您不能返回数组或分配给它。指针互换性旨在成为相同级别对象之间的概念:仅适用于标准布局类或联合体。
该概念在整个草案中很少使用:在[expr.static.cast]中出现作为特殊情况,在[class.mem]中,注释说对于标准布局类,指向对象及其第一个子对象的指针是可互换的,在[class.union]中,指向联合及其非静态数据成员的指针也被声明为可互换,并且在[ptr.launder]中。
最后一次出现将2个用例分开:要么指针是可互换的,要么一个元素是数组。这是在备注中而不是像[basic.compound]中那样在注释中说明的,因此更清楚地表明了指针互换性故意不涉及数组。

允许实现在对象指针和数组指针之间有不同的表示方式。如果一个指向数组的指针是其结构体super对象的第一个成员,那么为什么可以将其转换为指向该结构体对象的指针呢? - n. m.
@n.m.:经过一些思考,我认为数组指针不能转换为其超级对象的指针,因为数组不是标准布局对象。我的观点是数组的第一个元素是可转换为超级对象的指针,但没有指向数组的指针。当然,当转换为字节指针(C++17之前的char指针)时,所有三个指针都将转换为相同的指针,因为数组的第一个字节是其第一个元素的第一个字节。然后可以通过char*或reinterpret_cast将数组指针转换为其第一个元素的指针,... - Serge Ballesta
...但是没有任何保证指向数组的指针和指向对象的指针(第一个元素和超级对象)具有相同的表示形式。一个指向对象的指针可以被 static_cast 转换为指向子类的指针,反之亦然,但是子类对象可能位于不同的地址。 - Serge Ballesta
一个数组和它的第一个元素有相同的地址。这是由标准保证的。没有人谈论数组的第一个元素的子类对象。只谈论第一个元素本身。有一个保证,即数组可以转换为其标准布局的超级对象,因此这些指针必须具有相同的表示,或者表示上的差异无关紧要。 - n. m.
1
此外,struct {int i;}及其第一个元素是可转换为指针的,即使相应的指针明确允许具有不同的表示形式。 - n. m.
@n.m.:我必须承认我没有证据支持它,所以这只是一种观点。因此,即使我非常欣赏评论中的讨论,也与答案无关。已编辑。非常感谢反馈。 - Serge Ballesta

1
我仔细阅读了标准的这一部分,我的理解是,如果两个对象在类定义中“相互连接”(请注意,指针可互换的概念是针对类对象及其第一个非静态数据成员定义的),则它们是指针可互换的。它们指向同一地址。但是,由于它们的类型不同,我们需要使用reinterpret_cast运算符“转换”它们的指针类型。对于问题中提到的数组对象,数组和其第一个元素在类定义上没有“相互连接”,并且我们不需要转换它们的指针类型才能使用它们。它们只是指向同一地址。

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