将固定数组大小的指针/引用转换为较小大小是否合法

时间:2020-10-26 15:00:26

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

按照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]的引用将是无效的。


我将这个问题标记为,因为这是我对答案最感兴趣的版本,但是我很想知道这个答案在较新的版本中是否有所不同。

2 个答案:

答案 0 :(得分:8)

在任何语言版本中,除了外,这里没有什么要说的:类型完全无关。 C ++ 20确实允许从T (*)[N]T (*)[]的转换(对于引用也是如此),但这并不意味着您可以等效地对待两个不同的N。该规则最接近“引用”的位置是[conv.array] / 1(“结果是指向数组第一个元素的指针。”,该T[2]不存在在您的示例中)和在[defns.undefined]中的 note (“当本文档省略行为的任何明确定义时,可能会出现未定义的行为”)。

编译器不“捕获”您的部分原因是reinterpret_cast 有效,可以返回为对象的真实类型另一个reinterpret_cast用于通过接口“潜伏”它,该接口期望一个指针或对另一种类型的引用(但不要使用作为该类型!)。这意味着所给的代码是合法的,但是consumereceive的调用者的明显定义会一起导致未定义的行为。 (另一部分是,优化器通常会留下始终未定义的代码,除非它可以消除分支。)

答案 1 :(得分:1)

一个迟到的附加答案,它会产生评论的质量,但会远远超过允许的内容量:

首先:好问题!值得注意的是,如此明显的问题很难得到验证,甚至在专家之间也会产生很多混淆。值得一提的是,我已经经常看到该类别的代码......

先说一下未定义行为

我认为至少关于指针使用的问题是一个很好的例子,必须承认,语言的一个方面的理论未定义行为有时会被另外两个强大的方面“击败”:

  1. 是否有其他标准条款可以降低多个案例中相关方面的 UB 程度?是否有可能在标准中的优先级甚至彼此不明确的条款? (在 C++20 中仍然存在几个突出的例子,例如,参见 operator auto() 的转换类型 ID 处理......)。
  2. 是否有(图灵)可证明的论点,即任何理论和实际的编译器实现都必须按照您的预期运行,因为语言还存在其他限制,必须以这种方式确定?说即使 UB 可以古怪的意思,编译器可以应用“我可以在这里做我想做的事情,即使是最大的混乱”,这可能是可以证明的,确保其他指定的(!)语言方面决定了至少实际上是不可能的。

因此,关于第 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(static_cast(v)) 如果 T1 和 T2 都是标准布局 类型([basic.types])和T2的对齐要求是没有 比 T1 更严格,或者如果任一类型为 void。转换一个 “指向 T1 的指针”类型的纯右值到类型“指向 T2 的指针”(其中 T1 和 T2 是对象类型,其中 T2 的对齐要求是 不比 T1) 严格,并返回到其原始类型产生 原始指针值。任何其他此类指针的结果 转换未指定。

这乍一看很有希望,但问题在于细节。这仅确保您可以应用指针转换,因为两个数组的对齐要求是相等的,而不是先验地指代相互转换(即对象使用本身)。 正如戴维斯已经说过的那样:通过指向第一个元素的指针,只要错误的类型 reinterpret_cast 仅真正用作转发器,您仍然可以将 pointer to T[2] 用作某种完全符合标准的假外观并且所有实际用例都通过相应的 reinterpret_cast 引用元素指针,并且只要所有用例“知道”实际类型是 T[4] 的事实。很明显,这对于许多场景来说仍然是地狱般的。这里至少推荐一个类型别名,以强调转发质量。

所以这里对标准的严格解释是:这是未定义的行为,注意我们都知道它应该与许多常见平台上的所有常见现代编译器一起工作(我知道,后者不是你的问题)。< /p>

根据我的观点 2) 我们是否有机会从上面获得有效的“弱 UB”?

我不这么认为,只要这里只关注抽象机器即可。例如,IMO 没有标准的限制,编译器/环境无法在不同大小的数组之间以不同的方式处理(抽象)分配方案(例如更改阈值大小的内在函数),同时仍然确保对齐要求。在这里非常古怪,可以说一个非常奇特的编译器可以被允许引用底层的动态存储持续时间机制,即使对于看起来在我们所知的堆栈上的作用域对象也是如此。另一个相关的可能问题可能是关于在此处正确释放动态存储持续时间数组的问题(请参阅在不提供虚拟析构函数的类的继承上下文中关于 UB 的类似辩论)。我非常怀疑验证是微不足道的,标准保证先验有效的清理,即在所有情况下为您的示例有效调用 ~T[4]。