使用强制转换为"wrong"类型的指针算术

Pointer arithmetic using cast to "wrong" type

本文关键字:类型 指针 wrong 转换      更新时间:2023-10-16

我有一个结构数组,我有一个指向其中一个结构的成员的指针。 我想知道数组的哪个元素包含该成员。 以下是两种方法:

#include <array>
#include <string>
struct xyz
{
float x, y;
std::string name;
};
typedef std::array<xyz, 3> triangle;
// return which vertex the given coordinate is part of
int vertex_a(const triangle& tri, const float* coord)
{
return reinterpret_cast<const xyz*>(coord) - tri.data();
}
int vertex_b(const triangle& tri, const float* coord)
{
std::ptrdiff_t offset = reinterpret_cast<const char*>(coord) - reinterpret_cast<const char*>(tri.data());
return offset / sizeof(xyz);
}

下面是一个测试驱动程序:

#include <iostream>
int main()
{
triangle tri{{{12.3, 45.6}, {7.89, 0.12}, {34.5, 6.78}}};
for (const xyz& coord : tri) {
std::cout
<< vertex_a(tri, &coord.x) << ' '
<< vertex_b(tri, &coord.x) << ' '
<< vertex_a(tri, &coord.y) << ' '
<< vertex_b(tri, &coord.y) << 'n';
}
}

这两种方法都能产生预期的结果:

0 0 0 0
1 1 1 1
2 2 2 2

但它们是有效的代码吗?

特别是我想知道vertex_a()是否可能通过将float* y转换为xyz*来调用未定义的行为,因为结果实际上并不指向struct xyz。 这种担忧促使我写了vertex_b(),我认为这是安全的(是吗?

以下是使用 -O3 的 GCC 6.3 生成的代码:

vertex_a(std::array<xyz, 3ul> const&, float const*):
movq    %rsi, %rax
movabsq $-3689348814741910323, %rsi ; 0xCCC...CD
subq    %rdi, %rax
sarq    $3, %rax
imulq   %rsi, %rax
vertex_b(std::array<xyz, 3ul> const&, float const*):
subq    %rdi, %rsi
movabsq $-3689348814741910323, %rdx ; 0xCCC...CD
movq    %rsi, %rax
mulq    %rdx
movq    %rdx, %rax
shrq    $5, %rax

根据标准,两者都无效。


vertex_a中,您可以将指针转换为指向xyz::x指向xyz的指针,因为它们是指针可相互转换的:

两个对象ab指针可相互转换的,如果 [...] 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员 [...]

如果两个对象是指针可相互转换的,则它们具有相同的地址,并且可以通过reinterpret_­cast从指向另一个对象的指针获取指向一个对象的指针。

但是您不能从指针到xyz::y到指针到xyz进行强制转换。该操作未定义。


vertex_b中,您减去两个指向const char的指针。该操作在 [expr.add] 中定义为:

如果表达式PQ分别指向同一数组对象的元素x[i]x[j]x,则表达式P - Q的值为i − j;否则,行为未定义

您的表达式不指向char数组的元素,因此行为是未定义的。

vertex_a确实违反了严格的混叠规则(您的float都不是有效的xyzs,并且在您的示例中,50% 的示例中,即使没有填充,它们甚至不在xyz的开头)。

vertex_b依赖于,可以说,对标准的创造性解释。虽然你对const char*的投射是合理的,但在数组的其余部分使用它执行算术会更狡猾一些。从历史上看,我得出的结论是,这种东西有未定义的行为,因为在这种情况下,"对象"是xyz,而不是数组。但是,我现在倾向于其他人的解释,即这总是有效的,并且在实践中不会期望其他任何事情。

vertex_b

完全没问题。您可能只需要优化return offset / sizeof(xyz);,因为您将std::ptrdiff_tstd::size_t除法并将结果隐式转换为int。 根据书本,此行为是实现定义的。std::ptrdiff_t是有符号的,std::size_t无符号的,除法的结果可能大于在某些平台/编译器上具有巨大数组大小的INT_MAX(非常不可能)。

为了消除你的烦恼,你可以放assert()和/或#error来检查PTRDIFF_MINPTRDIFF_MAXSIZE_MAXINT_MININT_MAX,但我个人不会打扰这么多。

也许更健壮的方法涉及将类型签名更改为xyz::T*(T是一个模板参数,因此您可以根据需要采用xyz::xxyz::y)而不是float*

然后,您可以使用offsetof(struct xyz,T)自信地计算结构开始的位置,以便对其定义的未来更改更具弹性。

然后其余的就像你目前所做的那样:一旦你有一个指向结构开头的指针,在数组中找到它的偏移量是一个有效的指针减法。

这涉及到一些指针的肮脏。但这是一种使用的方法。例如,请参阅 Linux 内核中的 container_of() 宏。 https://www.linuxjournal.com/files/linuxjournal.com/linuxjournal/articles/067/6717/6717s2.html