按照C ++标准将指针或引用转换为固定数组(例如T(*)[N]
或T(&)[N]
)转换为指针或引用为相同类型的较小固定数组是否合法,并且简历资格(例如T(*)[M]
或T(&)[M]
)?
基本上,对于T
的所有实例化(无论布局类型如何),这始终应采用正确的格式:
void consume(T(&array)[2]);
void receive(T(&array)[6])
{
consume(reinterpret_cast<T(&)[2]>(array));
}
在以下位置,我看不到任何对此有效的引用:
但是,即使使用T = std::string
( compiler explorer )优化后,似乎所有主要的编译器都接受并生成适当的代码(不是证明很多) ,如果是未定义的行为)。
根据我的理解,根据类型系统,这应该是非法的,因为从未真正创建T[2]
的对象,这意味着对T(&)[2]
的引用将是无效的。
我将这个问题标记为c++11,因为这是我对答案最感兴趣的版本,但是我很想知道这个答案在较新的版本中是否有所不同。
答案 0 :(得分:8)
在任何语言版本中,除了否外,这里没有什么要说的:类型完全无关。 C ++ 20确实允许从T (*)[N]
到T (*)[]
的转换(对于引用也是如此),但这并不意味着您可以等效地对待两个不同的N
。该规则最接近“引用”的位置是[conv.array] / 1(“结果是指向数组第一个元素的指针。”,该T[2]
不存在在您的示例中)和在[defns.undefined]中的 note (“当本文档省略行为的任何明确定义时,可能会出现未定义的行为”)。
编译器不“捕获”您的部分原因是reinterpret_cast
的 有效,可以返回为对象的真实类型另一个reinterpret_cast
用于通过接口“潜伏”它,该接口期望一个指针或对另一种类型的引用(但不要使用作为该类型!)。这意味着所给的代码是合法的,但是consume
和receive
的调用者的明显定义会一起导致未定义的行为。 (另一部分是,优化器通常会留下始终未定义的代码,除非它可以消除分支。)
答案 1 :(得分:1)
一个迟到的附加答案,它会产生评论的质量,但会远远超过允许的内容量:
首先:好问题!值得注意的是,如此明显的问题很难得到验证,甚至在专家之间也会产生很多混淆。值得一提的是,我已经经常看到该类别的代码......
先说一下未定义行为
我认为至少关于指针使用的问题是一个很好的例子,必须承认,语言的一个方面的理论未定义行为有时会被另外两个强大的方面“击败”:
因此,关于第 2 点,有一个经常被低估的方面:抽象机器模型的约束(如果可定义)是什么,它决定了给定代码的任何理论(编译器)实现的结果?< /p>
到目前为止,有很多话,但是 1) 中的任何内容是否适用于您的具体情况(指针方式)?
正如用户在评论中多次提到的那样,机会就在这里basic.types#basic.compound-4:
<块引用>两个对象 a 和 b 是指针可相互转换的,如果:
...
(4.4) 存在一个对象 c 使得 a 和 c 是 指针可相互转换,c 和 b 是指针可相互转换的。
这就是传递性的简单规则。我们真的能找到这样的 c(对于数组)吗?
在同一部分,标准进一步说明:
<块引用>如果两个对象是指针可相互转换的,那么它们具有相同的 地址,并且可以从一个指针中获得一个指向 1 的指针 通过 reinterpret_cast 到另一个。 [ 注:一个数组对象及其 第一个元素不是指针可相互转换的,即使它们有 同一个地址。 — 尾注 ]
在这里通过指向第一个元素的指针 - 使用来摧毁我们对方法的梦想。数组没有这样的 c。
我们还有机会吗?你提到了expr.reinterpret.cast#7:
<块引用>对象指针可以显式转换为
一个不同的类型。 70 当一个“指向 T1 的指针”类型的纯右值 v 是
转换为“指向 cv T2 的指针”类型,结果为 static_cast
这乍一看很有希望,但问题在于细节。这仅确保您可以应用指针转换,因为两个数组的对齐要求是相等的,而不是先验地指代相互转换(即对象使用本身)。
正如戴维斯已经说过的那样:通过指向第一个元素的指针,只要错误的类型 reinterpret_cast
仅真正用作转发器,您仍然可以将 pointer to T[2]
用作某种完全符合标准的假外观并且所有实际用例都通过相应的 reinterpret_cast
引用元素指针,并且只要所有用例“知道”实际类型是 T[4]
的事实。很明显,这对于许多场景来说仍然是地狱般的。这里至少推荐一个类型别名,以强调转发质量。
所以这里对标准的严格解释是:这是未定义的行为,注意我们都知道它应该与许多常见平台上的所有常见现代编译器一起工作(我知道,后者不是你的问题)。< /p>
根据我的观点 2) 我们是否有机会从上面获得有效的“弱 UB”?
我不这么认为,只要这里只关注抽象机器即可。例如,IMO 没有标准的限制,编译器/环境无法在不同大小的数组之间以不同的方式处理(抽象)分配方案(例如更改阈值大小的内在函数),同时仍然确保对齐要求。在这里非常古怪,可以说一个非常奇特的编译器可以被允许引用底层的动态存储持续时间机制,即使对于看起来在我们所知的堆栈上的作用域对象也是如此。另一个相关的可能问题可能是关于在此处正确释放动态存储持续时间数组的问题(请参阅在不提供虚拟析构函数的类的继承上下文中关于 UB 的类似辩论)。我非常怀疑验证是微不足道的,标准保证先验有效的清理,即在所有情况下为您的示例有效调用 ~T[4]。