1 year ago

#336455

test-img

Ben

Understanding UB and pointer-arithmetic

I have a generic reference-counted heap-allocated wrapper class. So my class is basically just a pointer:

template <typename T>
class Refcounted {
    struct model { 
        std::atomic<std::size_t> count{1}; 
        T value;
    };
    model* m_self;
public:
    ...
    Refcounted(const Refcounted& other) : m_self(other.m_self) {
        assert(m_self);
        ++m_self->count;
    }
    ...
    T& operator*() { return m_model->value; }
};

Now, I'd like to be able to get a T& even in a context where T is forward-declared. As I understand it, I can't, because if T is just forward-declared, it can't know the layout of model (in particular, it can't know the offset of value because it can't know T's alignment).

I believe that if I swapped the order of model, it would be well-defined behavior to reinterpret_cast, correct?:

template <typename T>
class Refcounted {
    struct model { 
        T value;
        std::atomic<std::size_t> count{1}; 
    };
    model* m_self;
public:
    Refcounted(T&& x) : m_self(new model(std::move(x))) {
        static_assert(offsetof(model, value) == 0, "Using this assumption below to reinterpret_cast");
    }
    ...
    Refcounted(const Refcounted& other) : m_self(other.m_self) {
        assert(m_self);
        ++m_self->count;
    }
    ...
    T& operator*() { return *reinterpret_cast<T*>(m_model); }
};

Assuming that's correct, great... but now the copy-constructor requires T to be defined because it needs to find m_self->count. I had a thought for dealing with that, but I suspect it's UB: If I set up the model struct so that std::atomic<std::size_t> count is first and there's no padding between it and the T, then Refcounted keeps a void* pointer to the value field, as in m_valptr{&(new model(std::move(x)))->value}, then I could reinterpret_cast<T*>(m_valptr) to get at the value (which I think (?) is still well-defined-behavior). Is there any defined way to go from that pointer to a pointer to count? In principal, it's just decrementing the pointer by std::atomic<std::size_t>, but I suspect it breaks rules that I don't fully understand about what can and cannot be done with pointers.

I could add a second pointer to Refcounted or I could make model use a virtual interface, but that adds overhead. I feel like this should be possible but that there are spooky language rules getting in the way.

c++

undefined-behavior

pointer-arithmetic

reinterpret-cast

0 Answers

Your Answer

Accepted video resources