独特的指针和 3 法则

Unique Pointers and The Rule of 3

本文关键字:法则 指针      更新时间:2023-10-16

当我想要多态行为时,我经常发现自己在C++中使用独特的指针。我通常实现如下所示的纯抽象类:

class A { 
public:
virtual A* clone() const = 0; // returns a pointer to a deep copy of A
// other methods go here
};

当我想用自己的 A 实例修饰另一个类时,克隆方法会派上用场,例如:

#include <memory>
class B {
private:
std::unique_ptr<A> a_ptr;
public:
// ctor
B(const A& a) {
a_ptr = std::unique_ptr<A>(a.clone());
//...
}
// copy ctor
B(const B& other) : B(*other.a_ptr) {}
};

我总是最终在 B 中实现复制构造函数以避免编译器错误(MSVC 给出一条关于尝试引用已删除函数的模糊消息),由于唯一的指针,这完全有意义。我的问题可以总结如下:

  1. 我真的需要 B 中的复制构造函数吗?也许有一个更好的模式可以让我完全避免它。

  2. 如果是 1,我可以就此打住吗?我是否需要实现其他默认函数?即是否有任何情况我还需要默认构造函数和析构函数?

在实践中,每当我觉得我需要实现默认函数时,我通常会与其他三个函数一起实现一个 move-constructor;我通常使用复制和交换成语(根据 GManNickG 在此线程中的答案)。我认为这不会改变任何事情,但也许我错了!

多谢!

首先,我认为您的克隆函数的签名可能是

virtual std::unique_ptr<A> clone() = 0;

因为您希望A实例的深层副本和B中的独占所有权。其次,当您希望类可复制时,确实必须为类定义一个复制构造函数。赋值运算符也是如此。这是因为std::unique_ptr是仅移动类型,这会阻碍编译器生成默认实现。

不需要其他特殊成员函数,尽管它们可能有意义。编译器不会为您生成移动构造函数和移动赋值运算符(当您发布自己的复制/赋值函数时),尽管在您的情况下,您可以轻松= default;它们。析构函数同样可以用= default;来定义,这将符合核心准则。

请注意,通过= default定义析构函数应在翻译单元中完成,因为std::unique_ptr要求在释放其资源时知道完整类型。

是否需要默认构造函数完全取决于你希望如何使用类B

正如@lubgr在他的回答中提到的,你应该从clone函数中返回unique_ptr而不是原始的。无论如何,转到您的问题:

  1. 在 B 中需要复制构造函数吗?这取决于您的用例,但是如果您复制类B的对象,您可能需要一个。但正如你所说,你经常这样做,所以考虑更通用的方法是明智的。其中之一是为unique_ptr创建一个包装器,该包装器将具有复制构造函数,并将在此复制构造函数中对此指针进行深层复制。 请考虑以下示例:

    template<class T>
    class unique_ptr_wrap {
    public:
    unique_ptr_wrap(std::unique_ptr< T > _ptr) : m_ptr(std::move(_ptr)){}
    unique_ptr_wrap(const unique_ptr_wrap &_wrap){
    m_ptr = _wrap->clone();
    }
    unique_ptr_wrap(unique_ptr_wrap &&_wrap){
    m_ptr = std::move(_wrap.m_ptr);
    }
    T *operator->() const {
    return m_ptr.get();
    }
    T &operator*() const {
    return *m_ptr;
    }
    private:
    std::unique_ptr< T > m_ptr;
    };
    
  2. 这再次取决于您的需求。我个人也建议重载移动构造函数,以使其使用更少的动态分配(但这可能是万恶之源的预制优化)。