19

考虑到整个 C++11 标准,任何符合要求的实现是否有可能成功下面的第一个断言但失败了后者?

#include <cassert>

int main(int, char**)
{  
    const int I = 5, J = 4, K = 3;
    const int N = I * J * K;

    int arr1d[N] = {0};
    int (&arr3d)[I][J][K] = reinterpret_cast<int (&)[I][J][K]>(arr1d);
    assert(static_cast<void*>(arr1d) ==
           static_cast<void*>(arr3d)); // is this necessary?

    arr3d[3][2][1] = 1;
    assert(arr1d[3 * (J * K) + 2 * K + 1] == 1); // UB?
}

如果不是,这在技术上是否是 UB,如果第一个断言被删除,答案是否会改变(reinterpret_cast保证在这里保留地址?)?另外,如果重塑是在相反的方向(3d 到 1d)或从 6x35 阵列到 10x21 阵列进行的呢?

编辑:如果答案是因为 UB 是 UB reinterpret_cast,是否有其他严格兼容的重塑方式(例如,通过static_castto/from a intermediate void *)?

4

2 回答 2

26

2021 年 3 月 20 日更新:

最近在 Reddit 上提出了同样的问题,有人指出我原来的答案是有缺陷的,因为它没有考虑到这个别名规则

如果程序尝试通过类型与以下类型之一不相似的泛左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 对应于对象动态类型的有符号或无符号类型,或
  • char、unsigned char 或 std​::​byte 类型。

根据相似性规则,对于上述任何情况,这两种数组类型都不相似,因此通过 3D 数组访问 1D 数组在技术上是未定义的行为。(这绝对是其中一种情况,在实践中,它几乎可以肯定适用于大多数编译器/目标)

请注意,原始答案中的引用是指较旧的 C++11 草案标准

原答案:

reinterpret_cast参考文献

该标准规定,类型的左值T1可以是reinterpret_cast指向的引用,T2如果指向的指针T1可以reinterpret_cast指向指向的指针T2(第 5.2.10/11 节):

如果可以使用 reinterpret_cast 将类型“pointer to”的表达式显式转换为“pointer to ”类型,则可以将类型的左值表达式强制T1转换为“reference to ”类型。T2T1T2

所以我们需要确定是否int(*)[N]可以将 a 转换为int(*)[I][J][K]

reinterpret_cast指针

一个指针T1可以reinterpret_cast指向一个指针,T2如果两者T1都是T2标准布局类型并且T2没有比T1(§5.2.10/7)更严格的对齐要求:

当“指向 T1 的指针”类型的纯右值 v 转换为“指向 cv T2 的指针”类型时,结果是static_cast<cv T2*>(static_cast<cv void*>(v))如果T1T2都是标准布局类型(3.9)并且 的对齐要求T2不比 的更严格T1,或者如果任何一种类型都是无效的。

  1. int[N]int[I][J][K]标准布局类型吗?

    int是标量类型,标量类型的数组被认为是标准布局类型(第 3.9/9 节)。

    标量类型、标准布局类类型(第 9 条)、此类类型的数组以及这些类型的 cv 限定版本(3.9.3)统称为标准布局类型

  2. 没有int[I][J][K]比 更严格的对齐要求int[N]

    运算符的结果alignof给出了完整对象类型的对齐要求(第 3.11/2 节)。

    运算符的结果alignof反映了完整对象情况下类型的对齐要求。

    由于这里的两个数组不是任何其他对象的子对象,因此它们是完整的对象。应用于alignof数组给出了元素类型的对齐要求(§5.3.6/3):

    alignof应用于数组类型时,结果应为元素类型的对齐方式。

    所以这两种数组类型都有相同的对齐要求。

这使得reinterpret_cast有效且等效于:

int (&arr3d)[I][J][K] = *reinterpret_cast<int (*)[I][J][K]>(&arr1d);

where*&是内置运算符,则相当于:

int (&arr3d)[I][J][K] = *static_cast<int (*)[I][J][K]>(static_cast<void*>(&arr1d));

static_cast通过void*

标准转换(§4.10/2)允许to static_castvoid*

“指向 cv 的指针T”类型的纯右值,其中T是对象类型,可以转换为“指向 cv void 的指针”类型的纯右值。将“指向 cv 的指针T”转换为“指向 cv void 的指针”的结果指向类型对象所在的存储位置的开始T,就好像该对象是类型最派生的对象 (1.8) T(即,而不是基类子对象)。

然后允许static_castto int(*)[I][J][K](§5.2.9/13):

“指向 cv1 的指针void”类型的纯右值可以转换为“指向 cv2 的指针”类型的纯右值T,其中T是对象类型,且 cv2 与 cv1 具有相同的 cv 限定或大于 cv1 的 cv 限定。

所以演员阵容不错!但是我们可以通过新的数组引用访问对象吗?

访问数组元素

对数组执行数组下标arr3d[E2]等价于*((E1)+(E2))(§5.2.1/1)。让我们考虑以下数组下标:

arr3d[3][2][1]

首先,arr3d[3]等价于*((arr3d)+(3))。左值arr3d经过数组到指针的转换,得到一个int(*)[2][1]. 不要求底层数组必须是正确的类型才能进行此转换。然后访问指针值(第 3.10 节很好),然后将值 3 添加到它。这个指针算法也很好(§5.7/5):

如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

这个 this 指针被取消引用以给出一个int[2][1]. int这对接下来的两个下标进行相同的过程,从而在适当的数组索引处产生最终的左值。由于*(§5.3.1/1)的结果,它是一个左值:

一元 * 运算符执行间接:应用它的表达式应该是指向对象类型的指针,或指向函数类型的指针,结果是一个左值,指向表达式指向的对象或函数。

然后通过这个左值访问实际int对象是非常好的,因为左值也是类型int(第 3.10/10 节):

如果程序尝试通过非下列类型之一的泛左值访问对象的存储值,则行为未定义:

  • 对象的动态类型
  • [...]

所以除非我错过了什么。我会说这个程序定义明确。

于 2013-03-07T23:58:35.313 回答
1

我的印象是它会起作用。您分配同一块连续内存。我知道 C 标准保证它至少是连续的。我不知道 C++11 标准是怎么说的。

然而,第一个断言应该始终为真。数组的第一个元素的地址将始终相同。由于分配了同一块内存,因此所有内存地址都将相同。

因此,我还要说第二个断言将永远成立。至少只要元素的顺序始终按行主要顺序排列。C 标准也保证了这一点,如果 C++11 标准有不同的说法,我会感到惊讶。

于 2013-03-07T23:11:36.683 回答