自C ++ 17起,具有正确地址和类型的指针仍然始终是有效的指针吗?


84

(参考此问答)

在C ++ 17标准之前,[basic.compound] / 3中包含以下句子:

如果类型T的对象位于地址A,则将其值为地址A的cv T *类型的指针指向该对象,而不管如何获取该值。

但是从C ++ 17开始,此句子已删除

例如,我相信这句话使此示例代码已定义,并且由于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 ++ 17p1+1中,指针是一个end-the-end,因此它不是指向对象指针,并且我相信它是不可引用的。

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


注意:[basic.stc.dynamic.safety]和[util.dynamic.safety]中有关于指针出处的新/更新规则
MM

@MM仅对具有严格指针安全性的实现很重要,这是一个空集(在实验错误范围内)。
TC

4
引用的陈述实际上在实践中从未如此。给定的int a, b = 0;*(&a + 1) = 1;即使你检查也不能做&a + 1 == &b。如果仅通过猜测对象的地址即可获得指向该对象的有效指针,那么即使将局部变量存储在寄存器中也将成为问题。
TC

@TC 1)哪个编译器在获取地址后将var放入reg中?2)在不测量的情况下如何正确猜测地址?
curiousguy

@curiousguy正是这就是为什么简单地将通过其他方式(例如,猜测)获得的数字强制转换为对象恰好所在的地址是有问题的:它使该对象成为别名,但编译器并未意识到。相比之下,如果您使用对象的地址,如您所说:编译器将被警告并进行相应的同步。
彼得-恢复莫妮卡

Answers:


45

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

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

新的[basic.compound] / 3说:

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

这些是互斥的。p1+1是末尾的指针,而不是对象的指针。p1+1指向x[1]一个大小为1的数组的假设p1,而不是p2。这两个对象不是指针可互换的。

我们还有非规范性注释:

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

这澄清了意图。


正如TC在众多评论中指出的那样(特别是这一点),这确实是尝试实现问题的一种特例std::vector-这[v.data(), v.data() + v.size())需要是一个有效范围,但vector不会创建数组对象,因此只有定义的指针算法才能从向量中的任何给定对象到其假设的一维数组的末尾。有关更多资源,请参见CWG 2182此标准讨论以及有关该主题的论文的两个修订版:P0593R0P0593R1(特别是第1.3节)。


3
该示例基本上是已知的“可vector实施性问题”的特例。+1。
TC

2
@Oliv自C ++ 03起就存在一般情况。根本原因是指​​针算术无法按预期工作,因为您没有数组对象。
TC

1
@TC我相信唯一的问题来自指针算法的限制。这句话删除难道不会增加新的问题吗?该代码示例还是C ++ 17之前的UB版本吗?
奥利夫,

1
@Oliv如果指针算法是固定的,那么您p1+1将不再产生过去的指针,关于过去的指针的整个讨论都没有意义。您特殊的两元素特例可能不是UB pre-17,但这也不是很有趣。
TC

5
@TC您能否指出我可以阅读有关“向量可实现性问题”的地方?
SirGuy

8

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

动态对象创建在C ++标准的n4659草案的C ++对象模型[intro.object] §3中进行了描述:

3如果在与另一个对象“ e的类型为N个无符号字符的数组”或类型为“ N个std :: byte的数组”(21.2.1)的另一个对象e相关联的存储中创建了完整对象(8.3.4),则该数组将提供存储空间对于创建的对象,如果:
(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,并*p2buffer

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.coumpound]§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附件。消除在单个字符数组中动态创建的对象之间使用指针算法的可能性将是IMHO应该在此处引用的一项重要更改,因为它是常用的功能。由于兼容性页面中没有关于它的内容,我认为它确认该标准不是禁止它的意图。

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

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-Unslander Monica

@StoryTeller:我也希望。另外,在兼容性部分中对此没有任何提及。但似乎相反的意见在这里享有较高的声誉...
Serge Ballesta

2
您正在非规范性注释中抓住一个单词“无关”,并赋予它它不能承受的含义,这与管理指针算术的[expr.add]中的规范性规则相矛盾。附件C中没有任何内容,因为一般情况下的指针算法从未在任何标准中起作用。没有什么可打破的。
TC

3
@TC:Google对于找到有关此“向量可实现性问题”的信息非常无助,您能帮上忙吗?
Matthieu M.

6
@MatthieuM。请参阅核心问题2182std-讨论线程P0593R0P0593R1(尤其是1.3节)。基本的问题是vector不能(也不能)创建数组对象,但是具有一个接口,该接口允许用户获取支持指针算术的指针(仅针对指向数组对象的指针定义)。
TC

1

为了扩大此处给出的答案,我认为修改后的措词不包括以下示例:

警告:未定义的行为

#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因此,指针对象的值由对象的字节确定,而没有别的。因此,两个状态相同的对象指向同一对象。如果一个有效,则另一个也有效。因此,在常见的体系结构中,将一个指针值表示为一个数字,两个具有相等值的指针指向相同的对象,而其中一个指针指向相同的其他对象。
curiousguy

@Persixty另外,琐碎的类型意味着您可以枚举类型的可能值。本质上,任何处于优化模式的现代编译器(即使-O0在某些编译器上)也不会将指针视为琐碎的类型。编译器没有认真对待std的要求,编写std的人也没有认真对待它们,他们梦想着使用另一种语言,并且做出了各种与基本原理直接矛盾的发明。显然,当用户抱怨编译器错误时,他们会感到困惑,有时会受到不好的对待。
curiousguy

该问题中的非规范性注释要求我们认为“最后一句”没有指向任何内容。我们俩都知道在实践中很可能指向某物,并且在实践中有可能取消引用它。但这(根据标准)不是有效的程序。我们可以想象一个实现,该实现知道指针是通过最后一次算术获得的,并且在取消引用后会引发异常。虽然我知道可以做到这一点的平台。我认为该标准不想将其排除在外。
Persixty

@curiousguy另外,我不确定您列举的可能值是什么意思。这不是C ++定义的琐碎类型的必需功能。
Persixty
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.