【问题标题】:Reshaping a 1-d array to a multidimensional array将一维数组重塑为多维数组
【发布时间】:2013-03-07 23:01:37
【问题描述】:

考虑到整个 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 阵列进行?

编辑:如果答案是由于reinterpret_cast 而导致这是UB,是否还有其他一些严格合规的重塑方式(例如,通过static_cast 到/从中间@987654325 @)?

【问题讨论】:

  • 非常有趣的问题,而且我认为在没有 UB 的情况下也可以做到这一点,这要归功于“布局兼容”类型,尽管我不确定这些是否仅适用于结构/类 - 会必须调查。
  • 我很确定 C++11 规范实际上保证这种行为。
  • 那么,我是否需要检查第一个断言?地址是否保证相等?
  • 是的,但这是在对两种不同数组类型的引用之间进行转换
  • @MooingDuck:我们这里没有将数组的地址与其转换后的指针进行比较。

标签: c++ c++11 multidimensional-array language-lawyer


【解决方案1】:

2021-03-20 更新:

最近同样的问题是asked on Reddit,有人指出我原来的答案是有缺陷的,因为它没有考虑到这个aliasing rule

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

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

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

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

原答案:

reinterpret_cast 的参考文献

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

如果“T1 的指针”类型的表达式可以显式转换为“T2 的指针”类型,则T1 类型的左值表达式可以转换为“T2 的引用”类型使用 reinterpret_cast。

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

reinterpret_cast 的指针

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

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

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

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

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

  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);

其中*&amp; 是内置运算符,则相当于:

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

static_castvoid*

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

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

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

“指向 cv1 void”类型的纯右值可以转换为“指向 cv2 T”类型的纯右值,其中T 是对象类型,而 cv2 与 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 节):

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

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

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

【讨论】:

  • +1 表示标准报价,但必须考虑您的答案
  • 这很有帮助,不知道 reinterpret_cast 在这种情况下实际上还可以;对我来说一切都很好
  • 不幸的是,您遇到了冲突,因为没有可以在其上进行数组到指针转换的数组。
  • 如果T1T2 都是标准布局类型并且T2 没有更严格的类型,则指向T1 的指针可以是指向T2 的指针的reinterpret_castT1 (§5.2.10/7) 的对齐要求。 为什么这里的对齐要求是针对类型而不是指针值指定的?当T2 具有更严格的对齐要求并且指针对齐时,我不能将指针从T1 投射到T2 吗?
  • 通过3D数组访问1D数组在技术上是未定义的行为 不是UB,不可能,因为access to objects of array type is not a thing
【解决方案2】:

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

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

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

【讨论】:

  • "考虑到整个C++11标准,"
  • 在这种情况下,reinterpret_cast 的行为是否得到保证?这些是技术上不兼容的类型
  • @StephenLin "结果是指向数组第一个元素的指针" 所以 1d 变成了指向第一个 int 的指针。 3d 变成指向第一个 2d 数组的指针,它变成指向第一个 1d 数组的指针,又变成指向第一个 int 的指针。 same 第一个 int.
  • @AxelOmega:我不确定“行主要”是 3d 数组的正确术语。
  • @Xeo,我认为 to/from void * 对保留地址有一些特殊保证,我可能错了;不过,我不认为 reinterpret_cast 与不兼容的数组之间存在,这就是我这样写的原因
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-05-06
  • 2012-09-28
  • 1970-01-01
  • 2013-08-03
  • 2017-09-12
  • 2020-07-06
  • 2020-03-19
相关资源
最近更新 更多