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

时间:2018-01-02 14:00:35

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

(参考this question and answer。)

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

  

如果类型为T的对象位于地址A,那么类型为cv T *的指针(其值为地址A)将被指向该对象,而不管该值是如何获得的。

但是自从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,因此它不是指向对象的指针,我相信它不是可以解除引用的。

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

3 个答案:

答案 0 :(得分:44)

  

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

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

新的[basic.compound]/3说:

  

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

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

我们也有非规范性的说明:

  

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

澄清了意图。

作为T.C.在许多评论(notably this one)中指出,这是尝试实施std::vector时出现问题的一个特例 - 这是[v.data(), v.data() + v.size())需要有效范围的vector不会创建数组对象,因此唯一定义的指针算法将从向量中的任何给定对象传递到其假设的单一数组的过去结束。如需了解更多资源,请参阅CWG 2182this std discussion以及有关此主题的两篇文章修订:P0593R0P0593R1(具体见1.3节)。

答案 1 :(得分:8)

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

动态对象创建在 4.5 C ++对象模型[intro.object] ,n4659 C ++标准草案的第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或sub的子对象   (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附录。删除在单个字符数组中动态创建的对象之间使用指针算术的可能性将是一个重要的变化,恕我直言应该在那里引用,因为它是一个常用的功能。由于兼容性页面中没有任何相关内容,我认为它确认标准的目的不是禁止它。

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

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用作指向数组的第一个元素的指针...

答案 2 :(得分: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的“一个结尾”碰巧保存{{1}的第一个元素的地址值1}}。

修订后的规范确保无论B永远不是指向A+1的有效指针。旧的短语“无论如何获得值”都说如果'A + 1'碰巧指向'B [0]'那么它就是指向'B [0]'的有效指针。 这不可能是好的,也绝对不是意图。