Home > Back-end >  Am I invoking undefined behavior when casting back to my pointer?
Am I invoking undefined behavior when casting back to my pointer?

Time:01-06

I've been trying to figure out if this is an optimisation bug as it only seems to affect stack variables, and I wonder if there's some incorrect assumptions being made. I have this type which converts to and from a relative offset, and it has been working fine while using reinterpret_cast, but now I'm moving to static_cast, it's starting to cause issues in optimised builds. I need to get away from reinterpret_cast for safety certification reasons, so I don't have the option of keeping it as it is.

#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;
}

with optimisations turned on, I get output like this:

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

Which includes some output which is clearly inconsistent with itself.

I've tested this on GCC 8,9, and 11.2.

  • As soon as I drop back to -O0, it's fine.
  • If I uncomment the std::cout inside the operator==, it's fine.
  • If I go back to reinterpret_cast (return reinterpret_cast<T*>(reinterpret_cast<std::ptrdiff_t>(&m_offset) - offset);) it's fine.
  • If I allocate data as a pointer and initialise rp from that, it's fine too.
  • It seems to behave as I expect under clang (8 through 13 seem fine)

This feels like I am either not understanding where the UB is coming from, or there's a compiler optimisation bug here.


EDIT / UPDATE:

After looking at this is more detail I think the only solution is to do type punning in a semi-safe way, so I have tried this solution and it appears to work. (it appears, what I am now doing is part of C 20 and called bit_cast, so maybe this is valid?)

    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);
    }

This no longer seems to cause a problem with GCC. I hear that std::memcpy is meant to be used for objects of same type where otherwise we would have used reinterpret_cast, so this makes sense to me.

CodePudding user response:

Your undefined behaviour is in GetOffset.

This is how pointer subtraction is defined by the standard:

When two pointer expressions P and Q are subtracted, the type of the result is an implementation-defined signed integral type; this type shall be the same type that is defined as std::ptrdiff in the <cstddef> header ([support.types.layout])

  • If P and Q both evaluate to null pointer values, the result is 0.
  • Otherwise, if P and Q both point to, respectively, array elements i and j of the same array object x, the expression P - Q has the value i - j.
  • Otherwise, the behavior is undefined.

And here, P (which has the address of m_object) and Q (which has the address of data) aren't elements of the same array, so this is undefined behaviour.

Addition and subtraction of pointers and integers is also defined in terms of array elements:

When an expression J that has integral type is added to or subtracted from an expression P of pointer type, the result has the type of P.

  • If P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value.
  • Otherwise, if P points to an array element i of an array object x with n elements ([dcl.array]), the expression P J and J P (Where J has value j point to the (possibly-hypothetical) array element i j of x if 0≤i jn and the expression P - J points to the (possibly-hypothetical) array element i-j of x if 0≤i-j≤n.
  • Otherwise, the behavior is undefined.

The pointer subtraction happens at offset_address - offset, where P is the address of m_offset and offset is probably some positive number. m_offset is the first element of the array, so i-j < 0

So, the compiler can see that GetPtr returns a pointer relative to the m_offset (in the char[sizeof(Ptr<int>)] array that aliases the object), so it cannot equal the address of data (without UB), and thus an optimizer can replace (rp.get() == &data) with false.

When you use ptrdiff_t, no such restriction on addition or subtraction exists. And though the standard doesn't guarantee that reinterpret_cast<char*>(reinterpret_cast<intptr_t>(char_pointer) n) == char_pointer n (pointers mapping linearly as you would expect) this is what happens when compiling with gcc on common architectures.

  •  Tags:  
  • Related