将一维数组重塑为多维数组

20

考虑整个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,那么这是否在技术上属于UB?如果第一个断言被删除(即reinterpret_cast保证保留地址吗?)答案会改变吗?此外,如果将形状从3d到1d或从6x35数组到10x21数组的方向相反进行重塑怎么办?

编辑:如果由于reinterpret_cast而导致这是UB,是否有其他严格符合规范的重塑方法(例如通过将中间的void *静态转换为/从static_cast)?


2
非常有趣的问题,我认为可能可以通过“布局兼容”类型来实现,而不需要使用未定义行为(UB),尽管我不确定这些类型是否仅适用于结构体/类 - 我将进行调查。 - Xeo
我非常确定C++11规范实际上_保证_了这种行为。 - Mooing Duck
那么,我需要检查第一个断言吗?地址是否保证相等? - Stephen Lin
4.2 类型为“N个T的数组”或“未知边界T的数组”的左值或右值可以转换为类型为“指向T的指针”的prvalue。结果是指向数组第一个元素的指针。(地址保证相等) - Mooing Duck
1
是的,但这是在两种不同数组类型之间进行引用转换。 - Stephen Lin
1
@MooingDuck:我们这里并不是在比较数组的地址和其转换后的指针。 - Xeo
2个回答

27

更新时间:2021-03-20:

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

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

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

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

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

原始答案:

reinterpret_cast引用

标准规定,如果指向T1的指针可以通过reinterpret_cast转换为指向T2的指针,则类型为T1的lvalue可以reinterpret_cast为类型为T2的引用(§5.2.10/11):

如果类型为T1的lvalue表达式可以强制转换为类型“T1指针”的表达式,则可以将其转换为类型“T2的引用”,使用reinterpret_cast

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

reinterpret_cast指针

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

当将类型为“指向T1”的prvalue v转换为类型“指向cv T2”的指针时,如果T1和T2都是标准布局类型(3.9),并且T2的对齐要求不比T1更严格,或者任一类型为void,则结果为static_cast<cv T2*>(static_cast<cv void*>(v))
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应用于数组会给出元素类型的对齐要求(§5.3.6/3): - 当将alignof应用于数组类型时,结果应为元素类型的对齐要求。 - 因此,两个数组类型具有相同的对齐要求。
因此,reinterpret_cast是有效的并等效于:
int (&arr3d)[I][J][K] = *reinterpret_cast<int (*)[I][J][K]>(&arr1d);

其中*&是内置运算符,这等同于:

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

static_cast通过void*

标准转换(§4.10/2)允许将static_cast转换为void*

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

然后允许将static_cast转换为int(*)[I][J][K](§5.2.9/13):

类型为“指向 cv1 void 的指针”的 prvalue 可以转换为类型为“指向 cv2 T”的 prvalue,其中 T 是对象类型,cv2 是与 cv1 相同或更大的 cv-qualification。

因此,强制转换是正确的! 但是我们能通过新数组引用访问对象吗?

访问数组元素

对于像arr3d[E2]这样的数组执行下标操作相当于*((E1)+(E2))(§5.2.1/1)。让我们考虑以下数组下标操作:

arr3d[3][2][1]

首先,arr3d[3]等价于*((arr3d)+(3))。lvalue arr3d经历了数组到指针的转换,得到了一个int(*)[2][1]。没有要求底层数组必须是正确类型进行此转换。然后访问了指针的值(根据§3.10合法),并且将值3加到它上面。这种指针算术也是合法的(根据§5.7/5):
如果指针操作数和结果都指向同一数组对象或该数组对象之后的最后一个元素,则评估不应产生溢出;否则,行为未定义。
然后解除引用此指针以给出一个int[2][1]。对于下一个两个下标,进行相同的过程,导致最终适当数组索引处的int lvalue。由于*的结果(根据§5.3.1/1),它是一个lvalue:
一元*运算符执行间接引用:应用于它的表达式应该是一个对象类型的指针或函数类型的指针,结果是一个lvalue,它引用表达式指向的对象或函数。
然后,通过此lvalue访问实际的int对象是完全可以的,因为lvalue也是int类型(根据§3.10/10):
如果程序尝试通过除以下类型之一的glvalue访问对象的存储值,则行为未定义:
对象的动态类型
[...]
所以,除非我漏掉了什么,否则我会说此程序已经被定义良好。

1
+1 针对标准引用,您需要仔细思考您的回答。 - Stephen Lin
1
这很有帮助,我不知道在这种情况下reinterpret_cast实际上是可以的;对我来说一切听起来都很好。 - Stephen Lin
很遗憾,你遇到了没有可供进行数组指针转换的数组的问题。 - Davis Herring
如果T1T2都是标准布局类型,并且T2没有比T1更严格的对齐要求(§5.2.10/7),则可以将指向T1的指针重新解释为指向T2的指针。为什么这里的对齐要求是针对类型而不是指针值指定的?当T2具有更严格的对齐要求并且指针对齐时,我不能从T1转换为指向T2的指针吗? - Xeverous
访问1D数组通过3D数组在技术上是未定义行为。这不是UB,而是不可能的,因为访问数组类型对象不存在 - Language Lawyer

1
我认为它会起作用。您分配相同的连续内存块。我知道C标准保证至少是连续的。我不知道C++11标准中说了什么。
然而,第一个断言应该总是正确的。数组第一个元素的地址将始终相同。所有内存地址都将相同,因为分配了相同的内存块。
因此,我还会说第二个断言始终成立。至少只要元素的排序始终按行主顺序排列。这也由C标准保证,如果C++11标准有所不同,我会感到惊讶。

1
考虑到整个C++11标准, - Mooing Duck
在这种情况下,reinterpret_cast 的行为是否有保证呢?虽然这些类型在技术上是不兼容的。 - Stephen Lin
@AxelOmega:我不确定“行优先”是一个三维数组的正确术语。 - Mooing Duck
但重点是 reinterpret_cast,而不是 void * 部分。这是否保证保留地址? - Stephen Lin
1
@Xeo,我认为在转换 void * 时有一些特殊的保证可以保留地址,但我可能是错的;我不认为将不兼容的数组强制转换为/从 reinterpret_cast 具有这种保证,这就是为什么我以这种方式编写它的原因。 - Stephen Lin
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接