将 pointer/reference 转换为固定数组大小是否合法
Is it legal to convert a pointer/reference to a fixed array size to a smaller size
根据 C++ 标准,将指向固定数组的指针或引用(例如 T(*)[N]
或 T(&)[N]
)转换为指向相同数组的较小固定数组的指针或引用是否合法?类型和 CV 资格(例如 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 因为这是我对答案最感兴趣的版本,但我很想知道这个答案在新版本中是否有所不同。
除了没有,这里没什么可说的,在任何语言版本中:类型根本不相关。 C++20 确实允许从 T (*)[N]
到 T (*)[]
的转换(对于引用也类似),但这并不意味着您可以等同地对待两个不同的 N
。您最接近此规则的“参考”是 [conv.array]/1(“结果是指向数组第一个元素的指针。”,T[2]
在你的例子中不存在)和 [defns.undefined] 中的 note (“当本文档省略任何明确的行为定义时,可能会出现未定义的行为”)。
编译器不“捕捉”你的部分原因是这样的 reinterpret_cast
s 对 return 有效 到一个又一个对象的真实类型 reinterpret_cast
用于通过需要指针或引用不同类型的接口“潜入”它(但不 使用 就是那种类型!)。这意味着给定的代码是合法的,但是 consume
的明显定义和 receive
的调用者将 一起 导致未定义的行为。 (另一部分是,优化器通常不理会 总是 未定义的代码,除非它可以消除分支。)
一个迟到的附加答案,它产生了评论的质量,但会远远超过允许的内容量:
一开始:好问题!值得注意的是,这样一个非常明显的问题很难被证实,甚至在专家中也产生了很多混乱。值得一提的是,我已经经常看到该类别的代码了...
先说说未定义的行为
我认为至少关于指针使用的问题是一个很好的例子,人们必须承认,语言的一个方面的理论上未定义的行为有时会被其他两个强大的方面“打败”:
- 是否有其他标准条款可以降低几个案件的利益方面的UB程度?是否存在标准中的优先级甚至彼此不明确的条款? (C++20 中仍然存在几个突出的示例,例如,请参阅运算符 auto() 的转换类型 ID 处理...)。
- 是否存在(图灵-)可证明的论据,即任何理论和实际的编译器实现都必须按照您的预期运行,因为语言存在其他限制,必须以这种方式确定它?说即使 UB 可以古怪的意思,编译器可以为你的情况应用“我可以在这里做我想做的事,即使是最大的混乱”,这可能是可以证明的,确保其他指定的(!)语言方面决定了至少实际上是不可能的。
所以关于第 2 点,有一个经常被低估的方面:抽象机模型的约束(如果可定义)是什么,它们决定了给定代码的任何理论(编译器)实现的结果?
到目前为止,说了很多话,但是 1) 中的任何内容是否适用于您的具体案例(指针方式)?
正如评论中多次提到的用户,机会就在这里 basic.types#basic.compound-4:
Two objects a and b are pointer-interconvertible if:
...
(4.4) there exists an object c such that a and c are
pointer-interconvertible, and c and b are pointer-interconvertible.
这就是传递性的简单规则。我们真的可以找到这样的 c(用于数组)吗?
在同一节中,标准进一步说明:
If two objects are pointer-interconvertible, then they have the same
address, and it is possible to obtain a pointer to one from a pointer
to the other via a reinterpret_cast. [ Note: An array object and its
first element are not pointer-interconvertible, even though they have
the same address. — end note ]
通过指向第一个元素的指针 - 用法,打破我们对我们方法的梦想。数组没有这样的 c。
我们还有机会吗?你提到 expr.reinterpret.cast#7 :
An object pointer can be explicitly converted to an object pointer of
a different type.70 When a prvalue v of type “pointer to T1” is
converted to the type “pointer to cv T2”, the result is static_cast<cv
T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout
types ([basic.types]) and the alignment requirements of T2 are no
stricter than those of T1, or if either type is void. Converting a
prvalue of type “pointer to T1” to the type “pointer to T2” (where T1
and T2 are object types and where the alignment requirements of T2 are
no stricter than those of T1) and back to its original type yields the
original pointer value. The result of any other such pointer
conversion is unspecified.
乍一看这看起来很有希望,但细节决定成败。这仅确保您可以应用指针转换,因为两个数组的对齐要求是相等的,而不是先验地参考相互转换性(即对象使用本身)。
正如戴维斯已经说过的那样:使用指向第一个元素的指针,只要错误的类型 pointer to T[2]
仅真正用作转发器,人们仍然可以将 reinterpret_cast
用作某种完全符合标准的假门面并且所有实际用例都通过相应的 reinterpret_cast
引用元素指针,并且只要所有用例都“知道”这个事实,即实际类型是 T[4]
。很容易看出,对于许多场景来说,这仍然很糟糕。此处至少推荐使用类型别名以强调转发质量。
所以这里对标准的严格解释是:这是未定义的行为,注意我们都知道它应该适用于许多常见平台上的所有常见现代编译器(我知道,后者不是你的问题) .
根据我的第 2 点,我们是否有机会从上面关于有效的“弱 UB”?
我不这么认为,只要这里只关注抽象机器。例如,IMO 没有来自标准的限制,compiler/environment 无法在不同大小的数组之间以不同方式处理(抽象)分配方案(例如更改阈值大小的内在函数),同时仍然确保对齐要求。在这里非常古怪,可以说一个非常奇特的编译器可以被允许引用底层动态存储持续时间机制,即使对于似乎位于我们所知的堆栈上的作用域对象也是如此。另一个相关的可能问题可能是关于在此处正确释放动态存储持续时间数组的问题(请参阅不提供虚拟析构函数的 类 继承上下文中关于 UB 的类似辩论)。我非常怀疑验证是微不足道的,该标准保证先验地进行有效清理,即在所有情况下为您的示例有效调用 ~T[4]。
根据 C++ 标准,将指向固定数组的指针或引用(例如 T(*)[N]
或 T(&)[N]
)转换为指向相同数组的较小固定数组的指针或引用是否合法?类型和 CV 资格(例如 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 因为这是我对答案最感兴趣的版本,但我很想知道这个答案在新版本中是否有所不同。
除了没有,这里没什么可说的,在任何语言版本中:类型根本不相关。 C++20 确实允许从 T (*)[N]
到 T (*)[]
的转换(对于引用也类似),但这并不意味着您可以等同地对待两个不同的 N
。您最接近此规则的“参考”是 [conv.array]/1(“结果是指向数组第一个元素的指针。”,T[2]
在你的例子中不存在)和 [defns.undefined] 中的 note (“当本文档省略任何明确的行为定义时,可能会出现未定义的行为”)。
编译器不“捕捉”你的部分原因是这样的 reinterpret_cast
s 对 return 有效 到一个又一个对象的真实类型 reinterpret_cast
用于通过需要指针或引用不同类型的接口“潜入”它(但不 使用 就是那种类型!)。这意味着给定的代码是合法的,但是 consume
的明显定义和 receive
的调用者将 一起 导致未定义的行为。 (另一部分是,优化器通常不理会 总是 未定义的代码,除非它可以消除分支。)
一个迟到的附加答案,它产生了评论的质量,但会远远超过允许的内容量:
一开始:好问题!值得注意的是,这样一个非常明显的问题很难被证实,甚至在专家中也产生了很多混乱。值得一提的是,我已经经常看到该类别的代码了...
先说说未定义的行为
我认为至少关于指针使用的问题是一个很好的例子,人们必须承认,语言的一个方面的理论上未定义的行为有时会被其他两个强大的方面“打败”:
- 是否有其他标准条款可以降低几个案件的利益方面的UB程度?是否存在标准中的优先级甚至彼此不明确的条款? (C++20 中仍然存在几个突出的示例,例如,请参阅运算符 auto() 的转换类型 ID 处理...)。
- 是否存在(图灵-)可证明的论据,即任何理论和实际的编译器实现都必须按照您的预期运行,因为语言存在其他限制,必须以这种方式确定它?说即使 UB 可以古怪的意思,编译器可以为你的情况应用“我可以在这里做我想做的事,即使是最大的混乱”,这可能是可以证明的,确保其他指定的(!)语言方面决定了至少实际上是不可能的。
所以关于第 2 点,有一个经常被低估的方面:抽象机模型的约束(如果可定义)是什么,它们决定了给定代码的任何理论(编译器)实现的结果?
到目前为止,说了很多话,但是 1) 中的任何内容是否适用于您的具体案例(指针方式)?
正如评论中多次提到的用户,机会就在这里 basic.types#basic.compound-4:
Two objects a and b are pointer-interconvertible if:
...
(4.4) there exists an object c such that a and c are pointer-interconvertible, and c and b are pointer-interconvertible.
这就是传递性的简单规则。我们真的可以找到这样的 c(用于数组)吗?
在同一节中,标准进一步说明:
If two objects are pointer-interconvertible, then they have the same address, and it is possible to obtain a pointer to one from a pointer to the other via a reinterpret_cast. [ Note: An array object and its first element are not pointer-interconvertible, even though they have the same address. — end note ]
通过指向第一个元素的指针 - 用法,打破我们对我们方法的梦想。数组没有这样的 c。
我们还有机会吗?你提到 expr.reinterpret.cast#7 :
An object pointer can be explicitly converted to an object pointer of a different type.70 When a prvalue v of type “pointer to T1” is converted to the type “pointer to cv T2”, the result is static_cast<cv T2*>(static_cast<cv void*>(v)) if both T1 and T2 are standard-layout types ([basic.types]) and the alignment requirements of T2 are no stricter than those of T1, or if either type is void. Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. The result of any other such pointer conversion is unspecified.
乍一看这看起来很有希望,但细节决定成败。这仅确保您可以应用指针转换,因为两个数组的对齐要求是相等的,而不是先验地参考相互转换性(即对象使用本身)。
正如戴维斯已经说过的那样:使用指向第一个元素的指针,只要错误的类型 pointer to T[2]
仅真正用作转发器,人们仍然可以将 reinterpret_cast
用作某种完全符合标准的假门面并且所有实际用例都通过相应的 reinterpret_cast
引用元素指针,并且只要所有用例都“知道”这个事实,即实际类型是 T[4]
。很容易看出,对于许多场景来说,这仍然很糟糕。此处至少推荐使用类型别名以强调转发质量。
所以这里对标准的严格解释是:这是未定义的行为,注意我们都知道它应该适用于许多常见平台上的所有常见现代编译器(我知道,后者不是你的问题) .
根据我的第 2 点,我们是否有机会从上面关于有效的“弱 UB”?
我不这么认为,只要这里只关注抽象机器。例如,IMO 没有来自标准的限制,compiler/environment 无法在不同大小的数组之间以不同方式处理(抽象)分配方案(例如更改阈值大小的内在函数),同时仍然确保对齐要求。在这里非常古怪,可以说一个非常奇特的编译器可以被允许引用底层动态存储持续时间机制,即使对于似乎位于我们所知的堆栈上的作用域对象也是如此。另一个相关的可能问题可能是关于在此处正确释放动态存储持续时间数组的问题(请参阅不提供虚拟析构函数的 类 继承上下文中关于 UB 的类似辩论)。我非常怀疑验证是微不足道的,该标准保证先验地进行有效清理,即在所有情况下为您的示例有效调用 ~T[4]。