【问题标题】:Treating 2D array as 1D array将二维数组视为一维数组
【发布时间】:2018-11-20 13:44:08
【问题描述】:

假设我们有一个 2D int 数组:

int a[3][4] = { { 1,3,2,4 }, { 2,1,5,3 }, { 0,8,2,3 } };

获取其地址并将其重新解释为指向ints 的一维数组的指针是否合法有效?基本上:

int *p = reinterpret_cast<int *>(&a);

这样我就可以(大致)执行以下操作:

template<typename T, size_t X, size_t Y>
void sort2(T(&arr)[X][Y])
{
    T *p = reinterpret_cast<T *>(&arr);
    std::sort(p, p + X*Y);
}

演示:https://ideone.com/tlm190

据我所知,该标准保证二维数组的对齐在内存中是连续的,尽管p + X*Y 在技术上超出范围,但永远不会被访问,因此也不应该导致未定义的行为。

我是否可以在需要时将 2D 数组完全视为 1D 数组?

【问题讨论】:

  • 有两种思想流派……有人说这是合法的,有人说不合法。已经在 C 和 C++ 中争论了 30 年,但没有解决方案
  • @MM 谢谢!我以为我快疯了,因此问题:)
  • 不确定它是否与stackoverflow.com/questions/48444004/… 重复。它们本质上是相同的。
  • @xskxzr 相关,但不完全重复
  • 不管怎样T.C.'s answer也适用于这个问题。

标签: c++ arrays


【解决方案1】:

不能将二维数组视为一维数组

正如@KillzoneKid 所指出的,即使它们共享相同的地址值,数组的地址也不能与第一个元素的地址相互转换。

constexpr 提供了一种评估 UB 的便捷方法。在评估 constexprs 时,编译器需要检测 UB。这样就可以做一个简单的测试了。此函数在编译期间进行评估时将检测访问超出数组范围的值。

// sum "n" sequential ints
constexpr int sum(const int* pi, int n) {
    int sum = 0;
    while (n-- > 0)
        sum += *pi++;
    return sum;
};

当在运行时使用指向 1D 或 2D 数组的第一个元素的指针调用它时,它将对元素求和,但如果“n”超出数组的边界,则为 UB。对于二维数组,这将是最低维度的范围。不是整个数组的大小。

检查指向 int 的指针的不同实例,我们可以看到,一旦我们尝试访问超出数组维度的值,就会发生 UB。

int main()
{
    constexpr int i0{1};
    constexpr int i1[2]{ 1,2 };
    constexpr int i2[2][2] = { { 1,2}, {3,4} };

    constexpr int sum0 = sum(&i0, 1);       // fails for n>1
    constexpr int sum1 = sum(&i1[0], 2);    // fails for n>2
    constexpr int sum2 = sum(&i2[0][0], 2); // fails for n>2
    const int sum3 = sum(&i2[0][0], 4);     // runtime calc, UB. fails if constexpr

    return sum0 + sum1 + sum2 + sum3;       // 17
}

对于超出现有数据的访问,例如在sum0和sum1中,UB是明确的。但是 sum2 指向 n=[0:3) 的现有数据,但 constexpr 评估显示它是 n=4 的 UB。

得知这一点我有些惊讶。在执行诸如将矩阵的所有系数按固定量缩放之类的操作时,我曾经使用过的每个编译器都按预期工作。但是我可以看到基于优化假设的基本原理,即矩阵的某些部分不会因对不在同一数组序列中的另一部分的函数调用的结果而改变。

【讨论】:

    【解决方案2】:

    感谢大家的回复和评论,但我认为正确的答案是 - 就目前而言,代码展示了技术 UB,尽管可以纠正。我已经查看了其中一些问题 [1, 2] @xskxzr 已链接,它引导我找到this quote from the standard:

    如果两个对象是指针可互转换的,那么它们具有相同的 地址,并且可以从指针中获得指向一个的指针 通过 reinterpret_cast 到另一个。 [ 注意:一个数组对象及其 第一个元素不是指针可相互转换的,即使它们有 相同的地址。 — 尾注 ]

    然后在reinterpret_cast page 上有如下注释和一个例子:

    假设满足对齐要求,reinterpret_cast 会 在少数有限的情况下不改变指针的值 处理 pointer-interconvertible 对象:

    int arr[2];
    int* p5 = reinterpret_cast<int*>(&arr); // value of p5 is unchanged by reinterpret_cast and
                                            // is "pointer to arr"
    

    即使这个compiles without warning and runs,这在技术上是一个UB,因为p5在技术上仍然是一个指向arr而不是arr[0]的指针。所以基本上我使用reinterpret_cast的方式导致了UB。考虑到上述情况,如果我要直接创建int * 到第一个int(根据@codekaizer 的回答可以),那么这应该是有效的,对吧?:

    template<typename T, size_t X, size_t Y>
    void sort2(T(&arr)[X][Y])
    {
        T *p = &arr[0][0]; // or T *p = arr[0];
        std::sort(p, p + X * Y);
    }
    

    但它也可能是 UB,因为指针 p 指向具有 Y 元素的 Ts 的第一个数组的第一个 T。因此p + X*Y 将指向Ts 的第一个数组的范围之外,因此是UB(再次感谢@xskxzr 的link 和评论)。

    如果表达式 P 指向具有 n 的数组对象 x 的元素 x[i] 元素,表达式 P + J 和 J + P(其中 J 的值为 j) 如果 0≤i+j≤n,则指向(可能是假设的)元素 x[i+j]; 否则,行为未定义。

    所以这是我放弃前的最后一次尝试:

    template<typename T, size_t X, size_t Y>
    void sort2(T(&arr)[X][Y])
    {
        T(&a)[X * Y] = reinterpret_cast<T(&)[X * Y]>(arr);
        std::sort(a, a + X * Y);
    }
    

    这里T arr[X][Y] 首先转换为T a[X*Y],再次使用reinterpret_cast,我认为它现在是有效的。重新解释的数组a 愉快地衰减为指向数组a[X*Y] 的第一个元素的指针(a + X * Y 也在范围内)并转换为std::sort 中的迭代器。

    TL;DR 版本

    由于reinterpret_cast 使用不当,OP 中的行为未定义。将二维数组转换为一维数组的正确方法是:

    //-- T arr2d[X][Y]
    T(&arr1d)[X*Y] = reinterpret_cast<T(&)[X*Y]>(arr2d);
    

    T1 类型的左值表达式可以转换为引用 另一种T2。结果是一个左值或 xvalue,指的是 与原始左值相同的对象,但类型不同。不 临时创建,没有复制,没有构造函数或转换 函数被调用。结果引用只能访问 如果类型别名规则允许,则安全

    Aliasing rules:

    每当尝试读取或修改存储的值时 DynamicType 类型的对象通过 AliasedType 类型的左值, 除非满足以下条件之一,否则行为未定义:

    • AliasedType 和 DynamicType 类似。

    Type similarity:

    非正式地,两个类型是相似的 if,忽略顶层 简历资格

    • 它们都是相同大小的数组或都是未知边界的数组,并且数组元素类型相似。

    Array element type:

    在T D 的声明中,D 的格式为

    D1 [ constant-expression opt ] attribute-specifier-seq opt

    声明TD1中标识符的类型是 “derived-declarator-type-listT”,则D的标识符的类型为数组类型;如果D 标识符的类型包含自动type-specifier,则程序格式错误。 T 被称为数组元素类型;

    【讨论】:

    • 解决方案取决于二维数组可以被视为一维数组这一事实。详情可以看my post。
    • @xskxzr 再次感谢您的链接,我已经调整了答案
    • a 引用的一维数组在现实中并不存在,因此数组到指针的转换会导致未定义的行为。 T.C.'s answer 也适用于此。
    • @xskxzr 我通过告诉编译器将数据视为Ts 的单个数组来创建a。 a 是一个数组,sizeof(a) 返回正确的大小,所以编译器知道。我没有看到这里的问题,也没有看到它如何无法衰减为指针。
    • 您不能将二维数组视为一维数组,尽管它们具有相同的内存位置。您可以阅读 cppreference 的reinterpret_cast page:“类型 T1 的左值表达式可以转换为对另一种类型 T2 的引用。结果是左值或 xvalue 引用与原始左值相同的对象,但类型不同。没有临时创建,没有复制,没有调用构造函数或转换函数。只有在类型别名规则允许的情况下才能安全地访问结果引用(见下文)"。
    【解决方案3】:

    是的。合法有效。

    根据dcl.array:

    如果 E 是秩为 i×j×⋯×k 的 n 维数组,则 E 出现在 受 array-to-pointer 转换的表达式是 转换为指向 (n−1) 维数组的指针,其秩为 j×⋯×k。 如果 * 运算符,显式或隐式地由于 下标,应用于这个指针,结果是指向的 (n−1) 维数组,它本身立即转换为 指针。

    【讨论】:

    • 为什么这一段与问题有关?
    • 所以int a[3][4]可以转换为int (*)[4]。 OP 想一直到int*...
    【解决方案4】:

    来自http://www.cplusplus.com/doc/tutorial/arrays/

    int jimmy [3][5];   // is equivalent to
    int jimmy [15];     // (3 * 5 = 15)  
    

    当创建一个数组(任意维度)时,数组内存是size = sizeof(type) * dim0 * dim1 * ....;中的一个固定内存块

    所以对于您的问题,是的,您可以安全地将数组重铸为一维数组。

    【讨论】:

    • 值得注意的是,这不适用于动态创建的数组。
    • 它是“等价的”,但您不能将T[X][Y] 转换为T[X*Y]。一个是int** 类型,另一个是int*
    • @Vivick 实际上一个是int(*)[15] 类型,另一个是int(*)[3][5] 类型。
    • imho [3][5] 与 [15] 相同并不意味着您可以安全地在它们之间进行转换,它们仍然是不同的类型
    猜你喜欢
    • 2018-07-26
    • 1970-01-01
    • 2016-02-18
    • 2018-02-19
    • 2014-03-25
    • 1970-01-01
    相关资源
    最近更新 更多