【问题标题】:Am I invoking undefined behavior when casting back to my pointer?转换回指针时是否调用了未定义的行为?
【发布时间】:2022-01-05 12:12:50
【问题描述】:

我一直试图弄清楚这是否是一个优化错误,因为它似乎只影响堆栈变量,我想知道是否做出了一些不正确的假设。我有这种类型可以在相对偏移量之间进行转换,并且在使用reinterpret_cast 时工作正常,但现在我正在转向static_cast,它开始在优化构建中引起问题。出于安全认证的原因,我需要离开reinterpret_cast,所以我没有选择保持原样。

#include <iostream>

template <typename T>
class Ptr
{
public:
    Ptr(const T* ptr = nullptr) : m_offset(GetOffset(ptr)) {}
    T& operator*() const noexcept { return *GetPtr(); } 
    T* get() const noexcept { return GetPtr(); }

    bool operator==(const T *ptr) const {
        // comment this back in and it stops failing
        //std::cout << "{op==" << get() << " == " << ptr << "}";
        return get() == ptr;
    }

  private:
    std::ptrdiff_t m_offset = 0;

    inline T* GetPtr() const
    {
        auto offset = m_offset;
        auto const_void_address = static_cast<const void*>(&m_offset);
        auto const_char_address = static_cast<const char*>(const_void_address);
        auto offset_address = const_cast<char*>(const_char_address);
        auto final_address  = static_cast<void*>(offset_address - offset);
        return static_cast<T*>(final_address);
    }

    std::ptrdiff_t GetOffset(const void* ptr) const
    {
        auto void_address = static_cast<const void*>(&m_offset);
        auto offset_address = static_cast<const char*>(void_address);
        auto ptr_address = static_cast<const char*>(ptr);
        return offset_address - ptr_address;
    }
};

std::ostream& operator<<(std::ostream &stream, const Ptr<int> &rp) {
    stream << rp.get();
    return stream;
}

int main() {
    int data = 123;
    Ptr<int> rp(&data);
    std::cout << "data " << data << " @ " << &data << std::endl;
    std::cout << "rp " << *rp << " get " << rp.get() << std::endl;
    std::cout << (rp == &data) << std::endl;
    std::cout << "(rp.get() == &data) = " << (rp.get() == &data) << std::endl;
    std::cout << "(rp == &data) = " << (rp == &data) << std::endl;
    return 0;
}

启用优化后,我得到如下输出:

data 123 @ 0x7ffe79544a34
rp 123 get 0x7ffe79544a34
0
(rp.get() == &data) = 0
(rp == &data) = 0

其中包括一些与自身明显不一致的输出。

我已经在 GCC 8,9 和 11.2 上对此进行了测试。

  • 只要我回到-O0,就可以了。
  • 如果我取消注释 operator== 中的 std::cout,就可以了。
  • 如果我回到reinterpret_cast (return reinterpret_cast&lt;T*&gt;(reinterpret_cast&lt;std::ptrdiff_t&gt;(&amp;m_offset) - offset);) 就好了。
  • 如果我将数据分配为指针并从中初始化rp,也可以。
  • 它似乎在 clang 下表现得如我所料(8 到 13 看起来不错)

这感觉就像我不明白 UB 来自哪里,或者这里有一个编译器优化错误。


编辑/更新:

在查看了更多详细信息后,我认为唯一的解决方案是以半安全的方式进行类型双关语,所以我尝试了这个解决方案,它似乎有效。 (看来,我现在做的是 C++20 的一部分,叫做bit_cast,所以也许这是有效的?)

    inline T* GetPtr() const
    {
        auto offset = m_offset;
        intptr_t realAddress;
        auto address_of_m_offset = &m_offset;
        std::memcpy(&realAddress, &address_of_m_offset, sizeof( realAddress));
        realAddress -= m_offset;
        T *outValue;
        std::memcpy(&outValue, &realAddress, sizeof( outValue));
        return outValue;
    }

    std::ptrdiff_t GetOffset(const void* ptr) const
    {
        auto address_of_m_offset = &m_offset;
        intptr_t myAddress;
        std::memcpy(&myAddress, &address_of_m_offset, sizeof(myAddress));
        intptr_t realAddress;
        std::memcpy(&realAddress, &ptr, sizeof(realAddress));
        return static_cast<ptrdiff_t>(myAddress - realAddress);
    }

这似乎不再导致 GCC 出现问题。我听说 std::memcpy 旨在用于相同类型的对象,否则我们会使用 reinterpret_cast,所以这对我来说很有意义。

【问题讨论】:

  • 只是出于好奇:为什么要以这种方式存储指针?另外,只是猜测,可能是有符号值的溢出,这将是 UB?最后,我会添加“language-lawyer”标签,也许会替换“*-cast”标签。
  • 更简单的方法是比较优化和非优化“组装”的输出:)
  • 同一块物理内存在不同的虚拟地址空间同时使用时,不能使用绝对指针。这就是为什么我需要一个相对指针。这是共享内存问题的解决方案。

标签: c++ optimization language-lawyer undefined-behavior


【解决方案1】:

您的未定义行为在GetOffset 中。

标准是这样定义指针减法的:

当两个指针表达式PQ相减时,结果的类型是实现定义的有符号整数类型;此类型应与 &lt;cstddef&gt; 标头 ([support.types.layout]) 中定义为 std::ptrdiff 的类型相同

  • 如果PQ 都计算为空指针值,则结果为0
  • 否则,如果PQ分别指向同一个数组对象x的数组元素ij,则表达式@ 987654332@ 的值为 i - j
  • 否则,行为未定义。

这里,P(地址为m_object)和Q(地址为data)不是同一个数组的元素,因此这是未定义的行为。

指针和整数的加减法也是按照数组元素来定义的:

当一个整数类型的表达式J被添加到一个指针类型的表达式P或从中减去时,结果的类型为P

  • 如果P 的计算结果为空指针值,而J 的计算结果为0,则结果为空指针值。
  • 否则,如果P 指向具有n 个元素([dcl.array]) 的数组对象x 的数组元素i,则表达式@ 987654344@ 和 J + P(其中 J 的值 j 指向 x 的(可能是假设的)数组元素 i+j 如果 0≤i+jn 并且表达式 P - J 指向 x 的(可能是假设的)数组元素 i-j 如果 0≤i-j≤n.
  • 否则,行为未定义。

指针减法发生在offset_address - offset,其中Pm_offset 的地址,offset 可能是某个正数。 m_offset 是数组的第一个元素,所以 i-j

所以,编译器可以看到GetPtr返回一个相对于m_offset的指针(在给对象起别名的char[sizeof(Ptr&lt;int&gt;)]数组中),所以它不能等于data的地址(没有UB),因此优化器可以将(rp.get() == &amp;data) 替换为false

当您使用ptrdiff_t 时,不存在这样的加减限制。尽管标准不保证reinterpret_cast&lt;char*&gt;(reinterpret_cast&lt;intptr_t&gt;(char_pointer) + n) == char_pointer + n(如您所期望的那样线性映射​​指针),但在通用架构上使用 gcc 进行编译时会发生这种情况。

【讨论】:

  • 有趣的理论,但是当我将 reinterpret_cast 放回 GetOffset 时,行为并没有改变。我添加了return reinterpret_cast&lt;std::ptrdiff_t&gt;(&amp;m_offset) - reinterpret_cast&lt;std::ptrdiff_t&gt;(ptr);,它仍然存在同样的问题。问题似乎以某种方式存在于 GetPtr 中。 - 正如你所说,事实上它们不是来自同一个数组,对吧?
  • 如果没有reinterpret_cast 地形,我们是否处于“C++ 没有针对此问题的已定义行为解决方案”?
  • @RichardFabian 是的,即使您将 GetOffset 更改为使用 reinterpret_cast,那么表达式 offset_address - offset 将是 UB。如果你想使用加法和减法,你必须reinterpret_cast 以避免未定义的行为(而是有实现定义的行为)。
猜你喜欢
  • 2019-08-21
  • 2015-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-03
  • 2021-09-10
  • 2014-06-10
  • 2020-09-30
相关资源
最近更新 更多