【问题标题】:Pointer arithmetic using cast to "wrong" type使用强制转换为“错误”类型的指针算术
【发布时间】:2017-06-07 09:50:26
【问题描述】:

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

#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

【问题讨论】:

  • 这对the strict aliasing rule 造成了严重破坏。
  • @Someprogrammerdude:你能澄清一下吗?我认为vertex_b() 不会破坏严格混叠。至于vertex_a(),我不确定,因为指针永远不会被取消引用。
  • @Someprogrammerdude 不,不是。

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


【解决方案1】:

根据标准,两者均无效。


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

两个对象 abpointer-interconvertible 如果 [...] 一个是标准布局类对象而另一个是该对象的第一个非静态数据成员 [...]

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

但您不能将指向 xyz::y 的指针转换为指向 xyz 的指针。该操作未定义。


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

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

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

【讨论】:

  • 关于vertex_b(),请参阅此处的标准:stackoverflow.com/a/37119041/4323 - 它表示“如果程序尝试通过以下类型之一以外的 [...] 访问对象的存储值行为未定义 [...] - char 或 unsigned char 类型。”我确信这意味着在 reinterpret_casting 到 char* 之后从任何对象读取一个字节是有效的。因此,鉴于演员表是有效的,并且从生成的 char* 中读取字符是有效的,我认为这应该满足您的 [expr.add] 要求。你怎么看?
  • @John 这些都不会将您指向的内容转换为char 的数组。由于没有这些指针索引的数组,因此没有定义减法。
  • 好的,所以你要说的是,如果两个 char 指针指向的对象最初不是作为 char 类型的,那么对它们进行算术运算就永远不会好。那正确吗?你是说这会产生UB?考虑到这种事情在网络代码中的普遍性,这将是非常令人惊讶的(当然,这与首先利用将任何内容转换为 char* 的权利相同的代码)。
  • 另外,您是否有任何您认为完全合法的替代实现?
  • @Barry:在编写标准之前,C 和 C++ 都存在并且被广泛使用。将 C 中的对象和 C++ 中的 PODS 视为字符类型值序列的能力始终是这两种语言的基础。由于 C 标准的作者明确认识到它并没有强制要求所有必要的实现都可以用于任何目的,并且 C++ 标准依赖于 C 标准的关键方面,任何想要产生有用 i> 无论标准的确切措辞是否要求支持,实施都必须支持此类行为。
【解决方案2】:

vertex_a 确实违反了严格的别名规则(您的floats 都不是有效的xyzs,并且在您的示例的 50% 中,即使有没有填充)。

vertex_b 依赖于对标准的创造性 解释。尽管您对const char* 的强制转换是合理的,但在数组的其余部分使用它执行算术运算会更加狡猾。从历史上看,我已经得出结论,这种事情具有未定义的行为,因为这种情况下的“对象”是xyz,而不是数组。但是,我现在倾向于其他人的解释,即这将始终有效,并且在实践中不会期望其他任何东西。

【讨论】:

  • 我完全找不到我正在寻找的参考资料,但我会继续努力
  • 我认为严格的别名仅适用于指针被取消引用(在我的情况下不是)。我错了吗?
【解决方案3】:

vertex_b 完全没问题。您可能只需要细化return offset / sizeof(xyz);,因为您将std::ptrdiff_tstd::size_t 相除并将结果隐式转换为int。 按照书,这种行为是实现定义的。 std::ptrdiff_t 已签名,std::size_t 未签名,除法结果可能大于 INT_MAX(不太可能),在某些平台/编译器上具有巨大的数组大小。

为了摆脱你的烦恼,你可以把assert()s和/或#errors检查PTRDIFF_MINPTRDIFF_MAXSIZE_MAXINT_MININT_MAX,但我个人不会太麻烦了。

【讨论】:

  • vertex_a() 怎么样?有人建议它打破严格的别名,但我不明白如何,因为它不会取消引用指针。
  • 如果coord 可以指向xyz 的成员y,则vertex_a 是错误的。指针算法背后的主要思想,因为它出现(在 C 中),是指向元素,而不是任意内存位置(这些对于一个字节元素大小是相等的)。并且coord 可能不指向xyz 的开始,您甚至允许检查coord 的任何值。
  • 如果所需对齐的倍数不等于float 大小,则在这种情况下,内存对齐会被破坏。虽然许多 CPU 允许您将此地址存储到 CPU 寄存器中,并且在您尝试读取或写入它之前不会产生错误,但没有这样的保证。由于设计简化,一些 CPU(微控制器)甚至可能根本没有地址寄存器的前 N ​​个低位,甚至可能没有将这样的地址插入寄存器的指令,因为该指令也可能没有前 N 个低位。
【解决方案4】:

也许更健壮的方法是将类型签名更改为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

【讨论】:

  • 在某些情况下,我只有一个指向该成员的指针,并且不知道它的名称。我只知道地址。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-15
  • 2021-05-08
  • 1970-01-01
  • 2016-06-02
  • 1970-01-01
相关资源
最近更新 更多