为什么GCC会在这里优化掉赋值操作?

3

我有一个名为offset_ptr的类,它像指针一样工作,但将其所指内存地址作为相对于其自己的地址this的偏移量进行存储。下面是一个已删除了不必要内容以展示问题的版本:

template <typename T>
struct offset_ptr {
  using offset_t = int64_t;
  static constexpr auto const NULLPTR_OFFSET =
      std::numeric_limits<offset_t>::max();

  offset_ptr(T const* p)
      : offset_{p == nullptr ? NULLPTR_OFFSET
                             : static_cast<offset_t>(
                                   reinterpret_cast<uint8_t const*>(p) -
                                   reinterpret_cast<uint8_t const*>(this))} {}

  T* get() {
    return 
        offset_ == NULLPTR_OFFSET
            ? nullptr
            : reinterpret_cast<T*>(reinterpret_cast<uint8_t*>(this) + offset_);
  }

  offset_t offset_;
};

这段代码无法在使用GCC -O2-O3时正常运行:

int* get() {
  offset_ptr<int> ptr = static_cast<int*>(malloc(sizeof(int)));
  auto p = ptr.get();
  *p = 110;  // WOW - please do not optimize me away :-(
  return p;
}

(为保持简单,故意省略了内存管理和错误检查!)
在生成的汇编代码中也可以看到这一点: https://godbolt.org/z/PfZEJM 只是缺少赋值操作。
如上所示的GodBolt编译器资源管理器链接,在以下情况下可以工作:
- 分配的值直接在函数本身中使用。 - offset_ptr 位于堆上,而不是堆栈上。 - 不使用任何 offset_ptr
对于以下内容可以正常工作:
- Clang(带有和不带优化)。 - MSVC(调试模式和发布模式)。 - GCC(最新版本和旧版本)-O0-O1(但是没有-O2-O3)。
当执行时,GCC和Clang地址和UB sanitizer构建不会指示任何问题(除了泄漏的内存)。
有人能指出C++标准文档中哪个部分说此代码存在未定义行为(这可能是GCC积极优化掉赋值的原因)。或者这是GCC的一个错误?
编辑: 删除offset_ptr中的nullptr检查有帮助(https://godbolt.org/z/5HjcLY)。 但我需要这些空指针检查。
2个回答

2

[expr.add]p5:

当两个指针表达式P和Q相减时,结果的类型是一个实现定义的有符号整数类型;[...]
  • 如果P和Q都评估为空指针值,则结果为0。
  • 否则,如果P和Q分别指向同一数组对象x的元素x[i]和x[j],则表达式P-Q的值为i-j。
  • 否则,行为未定义。
成员初始化列表中的减法运算会回退到第三点,因此您会遇到未定义行为。
如果删除nullptr检查,则它可以“工作”,因为gcc无法证明第一个条件不会发生。

好的,谢谢。所以没有办法有一个 offset_ptr 类型吗?或者上面的代码可以修复吗? - Felix Gündling
我不这么认为,但说实话我不知道为什么你不直接存储指针。 - Rakete1111
好的。在将数据序列化到缓冲区时,这非常有用。像offset_ptr这样的类型(包括使用它的数据结构,例如自定义向量或unique_ptr类)可以通过仅进行转换来反序列化,当它存储指向序列化缓冲区中指向对象的偏移量时。 - Felix Gündling
啊,我明白了。但是,你怎么知道在thisp之间没有一些进程的内存 - 它们没有被写入缓冲区呢? - Rakete1111
1
@FelixGündling 您可以在序列化时而不是在内存中“实时”执行此操作。 在使用进程间共享内存块且位于两个不同地址空间同时解释这些位时,内存中的偏移指针确实非常重要。 - Yakk - Adam Nevraumont
显示剩余2条评论

1
如果你使用reinterpret_cast转换为uintptr_t而不是uint8_t *,那么你可以使其正常工作。这样你就将未定义行为替换为实现定义的行为。
参见: https://godbolt.org/z/rBTqYl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接