c++smart-pointerscopy-constructorcopy-assignment

How to implement copyable and movable wrapper around reference counted type?


Suppose a C API provides an opaque struct with internal reference counting:

struct Opaque {
    int data;
    int refcount;
};

struct Opaque* opaque_new(int data) {
    return new Opaque {
        .data = data,
        .refcount = 1
    };
}

int opaque_data(struct Opaque* opaque) {
    return opaque->data;
}

struct Opaque* opaque_ref(struct Opaque* opaque) {
    opaque->refcount++;
    return opaque;
}

void opaque_unref(struct Opaque* opaque) {
    opaque->refcount--;
    if (!opaque->refcount) {
        delete opaque;
    }
}

How to create a wrapper type that is copyable, movable, copy assignable and move assignable?

So far I have:

#include <algorithm>

class Wrapper {
public:
    Wrapper() : m_opaque(nullptr) {}

    explicit Wrapper(int data) : m_opaque(opaque_new(data)) {}

    Wrapper(const Wrapper&) = delete; // TODO

    Wrapper(Wrapper&& wrapper) : m_opaque(wrapper.m_opaque) {
        wrapper.m_opaque = nullptr;
    }

    Wrapper& operator=(const Wrapper&) = delete; // TODO

    Wrapper& operator=(Wrapper&& other) {
        swap(other);
        return *this;
    }

    ~Wrapper() {
        if (m_opaque) {
            opaque_unref(m_opaque);
        }
    }

    void swap(Wrapper& other) {
        std::swap(m_opaque, other.m_opaque);
    }

    int getData() const {
        if (m_opaque) {
            return opaque_data(m_opaque);
        } else {
            return 0;
        }
    }

private:
    struct Opaque* m_opaque;
};

I specifically don't want reference counting on top of reference counting, using std::shared_ptr with a custom deleter.

What's a concise way to implement the remaining methods, in particular copy assignment? Is there a better way to implement the ones I got so far?


Solution

  • Boost's intrusive_ptr was made for "internal reference counts":

    #include <boost/intrusive_ptr.hpp>
    
    // intrusive_ptr API functions
    inline void intrusive_ptr_add_ref(Opaque* opaque) noexcept {
        ::opaque_ref(opaque);
    }
    inline void intrusive_ptr_release(Opaque* opaque) noexcept {
        ::opaque_unref(opaque);
    }
    
    struct Wrapper {
    public:
        // Boost.intrusive_ptr operators are fine
        Wrapper() noexcept = default;
        Wrapper(const Wrapper&) noexcept = default;
        Wrapper(Wrapper&&) noexcept = default;
        Wrapper& operator=(const Wrapper&) noexcept = default;
        Wrapper& operator=(Wrapper&&) noexcept = default;
    
        // (false means don't add a refcount, since opaque_new does that)
        explicit Wrapper(int data) : m_opaque(opaque_new(data), false) {
            // if (!m_opaque) throw std::bad_alloc();
        }
    
        void swap(Wrapper& other) noexcept {
            m_opaque.swap(other.m_opaque);
        }
        friend void swap(Wrapper& l, Wrapper& r) noexcept {
            l.swap(r);
        }
    
        int getData() const {
            if (m_opaque) {
                return ::opaque_data(m_opaque.get());
            } else {
                return 0;
            }
        }
    private:
        boost::intrusive_ptr<Opaque> m_opaque;
    };
    

    https://godbolt.org/z/oGEdd66Yo

    You could also implement this with the "Copy-and-swap" idiom:

    Wrapper(Wrapper&& other) noexcept : m_opaque(std::exchange(other.m_opaque, nullptr)) {}
    
    Wrapper(const Wrapper& other) : m_opaque(other.m_opaque) {
        if (m_opaque) opaque_ref(m_opaque);
    }
    
    // Also works for move assign
    Wrapper& operator=(Wrapper copy) noexcept {
        this->swap(copy);
        return *this;
    }
    

    Unrelated, but your C function opaque_new should not throw C++ exceptions (when new fails). You can use new (std::nothrow) Wrapper { ... } so that it returns nullptr instead of throwing an exception. Or you could have it abort instead of throwing.
    And would it really hurt to have a nullptr check inside the functions?