使用强制转换为“错误”类型的指针算法

时间:2017-06-07 09:50:26

标签: c++ language-lawyer undefined-behavior pointer-arithmetic

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

#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(),我认为这是安全的(是吗?)。

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

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

4 个答案:

答案 0 :(得分:8)

根据标准,两者都无效。

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

  

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

     

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

但是你不能从指向xyz::y的指针到指向xyz的指针进行转换。该操作未定义。

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

  

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

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

答案 1 :(得分:4)

vertex_a确实打破了严格的别名规则(您的float没有一个是有效的xyz,而在您的示例的50%中,他们甚至没有在开始时即使没有填充,也要xyz

vertex_b依赖于,创意对标准的解释。虽然你对const char*的演员是合理的,但是在阵列的其余部分周围执行算术会更加狡猾。从历史上看,我已经得出结论,这种事物具有未定义的行为,因为&#34;对象&#34;在这个上下文中是xyz,而不是数组。但是,我倾向于其他人&#39;现在解释这将永远有效,并且在实践中不会期待任何其他事情。

答案 2 :(得分:3)

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_MAX,{{1} }和INT_MIN,但我个人不会那么烦。

答案 3 :(得分:1)

更强大的方法可能涉及将类型签名更改为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