如何围绕引用计数类型实现可复制和可移动的包装器?

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

提问人:Niklas 提问时间:9/7/2022 最后编辑:Niklas 更新时间:9/8/2022 访问量:164

问:

假设 C API 提供了一个具有内部引用计数的不透明结构:

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

如何创建可复制、可移动、可复制可分配和可移动可分配的包装器类型?

到目前为止,我有:

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

我特别不希望在引用计数之上进行引用计数,而是与自定义删除器一起使用。std::shared_ptr

实现其余方法(特别是复制分配)的简明方法是什么?有没有更好的方法来实现我目前得到的那些?

C++ 智能指针 构造函数 复制 赋值

评论

0赞 gerum 9/7/2022
在任意复制运算符中调用opaque_ref并保存返回的值
0赞 Niklas 9/7/2022
@gerum 复制分配还必须销毁旧值。我可以以某种方式使用析构函数吗,类似于移动分配的交换技巧?
0赞 gerum 9/7/2022
Sry,我错过了那部分。如果你想复制构造一个局部变量,然后交换局部变量和这个。
1赞 oraqlle 9/8/2022
@Niklas,您可以将传递给复制 ctor/assign 的参数的所有权移动到仅存在于 ctor/assign 作用域的临时变量。当它离开范围时,这将自动调用 temp 的 dtor 并相应地销毁资源,并且还具有(可能,取决于您的用例)将传递的参数保持在“未指定状态”的好处,然后您可以稍后使用(即仍然是“构造”但不拥有数据)。

答:

0赞 oraqlle 9/7/2022 #1

我假设您想要 Wrapper 对象的离散副本,因此您想要指向新数据而不是相同的数据(因此使用 copy ctor/assignment)。在这种情况下,您只需创建一个指向不透明类型的新指针,从传递给 ctor/assignment-operator 的不透明类型中复制值。m_opaque

注意1:对于复制分配,我添加了一个if,用于检查传递的不是“this”(即)。如果是这样,它只会自行返回而不会产生副作用。wrapWrapperw1 = w1

注意2:我还添加了一种方法,使它看起来更简洁。您可以将其设为私有/公共或其他任何内容,也可以直接使用 .getRefCount()->

例如。

class Wrapper
{
  /// Other constructors ...

  Wrapper(const Wrapper wrap)
  {
    m_opaque = new Opaque { 
                      .data = wrap.getData(), 
                      .refcount = wrap.getRefCount() 
                   };
  }

  Wrapper& operator=(const Wrapper wrap)
  {
    if (*this != wrap)
    {
      m_opaque = new Opaque { 
                        .data = wrap.getData(), 
                        .refcount = wrap.getRefCount() 
                     };
    }
    
    return *this;
  }

  int getRefCount()
  { return m_opaque->refcount; }

  /// Other methods ...
};

评论

1赞 gerum 9/8/2022
我假设他不想要这个,因为如果你想复制真实数据,也使用 Wrapper = Opaque 也可以。
0赞 oraqlle 9/8/2022
这是真的,但从我从问题中得到的是,即。复制 CTOR 用于制作对象的副本。 不是一个复制CTOR,而是一种显式的构造函数。how to make a copy ctor for a wrapper typeWrapperWrapperWrapper(const Opaque& oq)
0赞 gerum 9/8/2022
你如何获得那个转换构造函数,没有人谈论这样的事情。另一种解释是,模仿shared_ptr的行为,它有一个复制者,但它不复制数据。但最终的答案只能来自OP。
3赞 Artyer 9/8/2022 #2

Boost 的intrusive_ptr是针对“内部参考计数”而制定的:

#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

你也可以用“复制和交换”的习惯法来实现这一点:

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

不相关,但 C 函数不应引发 C++ 异常(失败时)。您可以使用它返回而不是引发异常。或者你可以让它中止而不是扔掉。
在函数内部进行检查真的会有什么坏处吗?
opaque_newnewnew (std::nothrow) Wrapper { ... }nullptrnullptr