【问题标题】:Is a pointer with the right address and type still always a valid pointer since C++17?自 C++17 以来,具有正确地址和类型的指针是否仍然始终是有效指针?
【发布时间】:2018-06-12 05:46:12
【问题描述】:

(参考this question and answer.)

在C++17标准之前,[basic.compound]/3中包含了下面这句话:

如果一个类型为 T 的对象位于地址 A 处,则称其值为地址 A 的类型为 cv T* 的指针指向该对象,而不管该值是如何获得的。

但是从C++17开始,这句话一直是removed

例如,我相信这句话定义了这个示例代码,并且从 C++17 开始,这是未定义的行为:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

在 C++17 之前,p1+1 持有指向*p2 的地址并且具有正确的类型,因此*(p1+1) 是指向*p2 的指针。在 C++17 中,p1+1pointer past-the-end,所以它不是 指向对象的指针,我相信它是不可取消引用的。

这是对标准权修改的这种解释,还是有其他规则可以补偿引用句子的删除?

【问题讨论】:

  • 注意:在 [basic.stc.dynamic.safety] 和 [util.dynamic.safety] 中有关于指针来源的新/更新规则
  • @M.M 这只对具有严格指针安全性的实现很重要,它是一个空集(在实验错误范围内)。
  • 引用的陈述在实践中从未真正成立过。给定int a, b = 0;,即使您检查了&a + 1 == &b,您也不能执行*(&a + 1) = 1;。如果仅仅通过猜测地址就可以得到一个有效的指向对象的指针,那么即使将局部变量存储在寄存器中也是有问题的。
  • @T.C. 1) 哪个编译器在您获取其地址后将 var 放入 reg 中? 2)如何在不测量地址的情况下正确猜测地址?
  • @curiousguy 正是这就是为什么简单地将通过其他方式(例如,猜测)获得的数字转换为对象恰好所在的地址是有问题的:它为该对象起别名,但编译器不知道它。相反,如果您按照您所说的那样获取对象的地址:编译器会收到警告并相应地同步。

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


【解决方案1】:

这是对标准权利的这种修改的解释,还是有其他规则来补偿删除引用的句子?

是的,这个解释是正确的。结束后的指针不能简单地转换为恰好指向该地址的另一个指针值。

新的[basic.compound]/3 说:

指针类型的每个值都是以下之一:
(3.1) 指向对象或函数的指针(该指针被称为指向对象或函数),或
(3.2) 超过对象末尾的指针([expr.add]),或

它们是相互排斥的。 p1+1 是一个指针,而不是指向对象的指针。 p1+1 指向位于 p1 的大小为 1 的数组的假设 x[1],而不是 p2。这两个对象不是指针可相互转换的。

我们也有非规范性注释:

[ 注意:超过对象末尾的指针([expr.add])不被认为指向可能位于该地址的对象类型的不相关对象。 [...]

这阐明了意图。


作为 T.C.在众多 cmets (notably this one) 中指出,这确实是尝试实现 std::vector 时出现的问题的一个特例——即 [v.data(), v.data() + v.size()) 需要是一个有效范围,而 vector 不是创建一个数组对象,因此唯一定义的指针算法将是从向量中的任何给定对象到其假设的单一大小数组的末尾。如需更多资源,请参阅 CWG 2182this std discussion 以及有关该主题的论文的两个修订版:P0593R0P0593R1(具体为第 1.3 节)。

【讨论】:

  • 这个例子基本上是已知的“vector 可实现性问题”的一个特例。 +1。
  • @Oliv 一般情况自 C++03 以来就存在。根本原因是指​​针算法没有按预期工作,因为您没有数组对象。
  • @T.C.我相信唯一的问题来自对指针运算的限制。这句话删了是不是又增加了一个新问题?代码示例也是 C++17 之前的 UB 吗?
  • @Oliv 如果指针算法是固定的,那么你的p1+1 将不再产生一个过去的指针,关于过去的指针的整个讨论是没有意义的。您特定的两元素特例可能不是 UB pre-17,但也不是很有趣。
  • @T.C.你能指出我可以阅读这个“向量可实现性问题”的地方吗?
【解决方案2】:

在您的示例中,*(p1 + 1) = 10; 应该是 UB,因为它是 数组末尾之后的一个,大小为 1。但我们这里是一个非常特殊的情况,因为数组是动态的构造在一个更大的 char 数组中。

动态对象创建在 4.5 C++ 对象模型 [intro.object],C++ 标准的 n4659 草案第 3 节中描述:

3 如果在与另一个类型为“N 的数组”的对象 e 关联的存储中创建了一个完整的对象(8.3.4) unsigned char”或“N std::byte 数组”(21.2.1) 类型,该数组为创建的 对象如果:
(3.1) — e 的生命周期已经开始但没有结束,并且
(3.2) — 新对象的存储完全适合 e,并且
(3.3) — 没有更小的数组对象可以满足这些约束。

3.3 似乎不太清楚,但下面的示例使意图更加清晰:

struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

所以在示例中,buffer 数组*p1*p2 提供存储

以下段落证明*p1*p2 的完整对象是buffer

4 一个对象 a 嵌套在另一个对象 b 中,如果:
(4.1) — a 是 b 的子对象,或
(4.2) — b 为 a 提供存储,或
(4.3)——存在一个对象c,其中a嵌套在c中,c嵌套在b中。

5 对于每个对象 x,都有一个对象称为 x 的完整对象,确定如下:
(5.1) — 如果 x 是一个完整的对象,那么 x 的完整对象就是它自己。
(5.2) — 否则,x 的完整对象是包含 x 的(唯一)对象的完整对象。

一旦确定,C++17 的 n4659 草案的其他相关部分是 [basic.compound] §3(强调我的):

3 ... 每个 指针类型的值是以下之一:
(3.1) — 指向对象或函数的指针(指针被称为指向对象或函数),或
(3.2) — 超过对象末尾的指针 (8.7),或
(3.3) — 该类型的空指针值 (7.11),或
(3.4) — 无效的指针值。

指针类型的值是指向或超过对象末尾的指针,表示对象的地址 对象占用的内存中的第一个字节(4.4)或存储结束后内存中的第一个字节 分别被物体占据。 [注意:超过对象(8.7)结尾的指针不被认为是 指向可能位于该地址的对象类型的不相关对象。一个指针值 当它表示的存储达到其存储期限结束时变为无效;见 6.7。 ——尾注] 出于指针算术 (8.7) 和比较 (8.9, 8.10) 的目的,指针经过最后一个元素的末尾 n 个元素的数组 x 被认为等价于指向假设元素 x[n] 的指针。这 指针类型的值表示是实现定义的。指向布局兼容类型的指针应 具有相同的值表示和对齐要求(6.11)...

注释指针越过结尾... 在这里不适用,因为p1p2 所指向的对象并不不相关,而是嵌套到同一个完整对象中,因此指针算术在提供存储的对象内部是有意义的:p2 - p1 被定义为 (&buffer[sizeof(int)] - buffer]) / sizeof(int) 即 1。

所以p1 + 1 指向*p2 的指针,*(p1 + 1) = 10; 已定义行为并设置*p2 的值。


我还阅读了关于 C++14 与当前 (C++17) 标准之间兼容性的 C4 附件。消除在单个字符数组中动态创建的对象之间使用指针算术的可能性将是一个重要的变化,恕我直言,应该在那里引用,因为它是一个常用的特性。由于兼容性页面中没有关于它的任何内容,我认为它证实了标准的意图不是禁止它。

特别是,它会破坏从没有默认构造函数的类中常见的对象数组动态构造:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr 然后可以用作指向数组第一个元素的指针...

【讨论】:

  • 啊哈,所以这个世界并没有变得疯狂。 +1
  • @StoryTeller:我也希望如此。此外,在兼容性部分中只字不提。但看起来相反的意见在这里有更多的声誉......
  • 您在非规范注释中抓住了一个词“无关”,并赋予它无法承受的含义,这与 [expr.add] 中管理指针算术的规范规则相矛盾.附件 C 中没有任何内容,因为通用情况指针算法从未在任何标准中工作过。没有什么可以打破的。
  • @T.C.:谷歌在查找有关“向量可实现性问题”的任何信息方面非常无助,您能帮忙吗?
  • @MatthieuM。请参阅core issue 2182this 标准讨论线程、P0593R0P0593R1 (particularly section 1.3)。基本问题是vector 没有(也不能)创建数组对象,但有一个接口允许用户获取支持指针运算的指针(仅针对指向数组对象的指针定义)。
【解决方案3】:

为了扩展这里给出的答案是我认为修改后的措辞不包括的一个例子:

警告:未定义的行为

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

由于完全依赖于实现(和脆弱)的原因,该程序的可能输出是:

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

该输出显示这两个数组(在这种情况下)恰好存储在内存中,因此A 的“末尾”恰好保存了B 的第一个元素的地址值。

修订后的规范确保A+1 永远不是指向B 的有效指针。古老的短语“不管值是如何获得的”表示如果“A+1”恰好指向“B[0]”,那么它就是指向“B[0]”的有效指针。 这不可能是好事,当然也不是这个意图。

【讨论】:

  • 这是否也有效地禁止在结构末尾使用空数组,以便派生类或自定义分配器 new 可以指定自定义大小的数组?也许新的问题是“无论如何”——有些方法是有效的,有些方法是危险的?
  • @Persixty 所以指针对象的值由对象的字节决定,仅此而已。所以状态相同的两个对象指向同一个对象。如果一个是有效的,另一个也是。所以在常见的架构中,指针值表示为一个数字,两个具有相同值的指针指向相同的对象,其中一个指向相同的其他对象。
  • @Persixty 另外,普通类型意味着您可以枚举类型的可能值。本质上,任何优化模式下的任何现代编译器(甚至某些编译器上的-O0)都不会将指针视为微不足道的类型。编译器不认真对待std的要求,编写std的人也不认真,他们梦想着一种不同的语言,做出各种直接违背基本原则的发明。显然,当用户抱怨编译器错误时,他们会感到困惑并且有时会受到不好的对待。
  • 问题中的非规范性注释希望我们将“过去的最后”视为不指向任何东西。我们都知道在实践中很可能指向某些东西,并且在实践中可能会取消引用它。但这(根据标准)不是一个有效的程序。我们可以想象一个实现,它知道指针是通过算术到过去的结尾获得的,如果取消引用,则会引发异常。虽然我知道知道这样做的平台。我认为标准不想排除它。
  • @curiousguy 另外,我不确定您列举可能的值是什么意思。这不是 C++ 定义的普通类型的必需特性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-29
相关资源
最近更新 更多