唯一指针和 3 法则
Unique Pointers and The Rule of 3
当我需要多态行为时,我经常发现自己在 C++ 中使用唯一指针。我通常实现如下所示的纯抽象 classes:
class A {
public:
virtual A* clone() const = 0; // returns a pointer to a deep copy of A
// other methods go here
};
当我想用它自己的 A 实例修饰另一个 class 时,克隆方法就派上用场了,例如:
#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 给出了关于尝试引用已删除函数的模糊消息),因为唯一指针,这是完全有意义的。我的问题可以总结如下:
我真的需要 B 中的复制构造函数吗?也许有一个更好的模式可以让我完全避免它。
如果 1 是,我可以就此打住吗?我是否需要实现其他默认功能? IE。有没有我还需要默认构造函数和析构函数的场景?
实际上,每当我觉得需要实现默认函数时,我通常会在其他三个函数的旁边实现一个移动构造函数;我通常使用 copy-and-swap-idiom(根据 GManNickG's answer in this thread)。我认为这不会改变任何东西,但也许我错了!
非常感谢!
首先,我认为你的克隆函数的签名可以是
virtual std::unique_ptr<A> clone() = 0;
因为您想要 A
个实例的深层副本和 B
中的独占所有权。其次,当您希望 class 可复制时,您确实必须为它定义一个复制构造函数。赋值运算符也是如此。这是因为 std::unique_ptr
是一个只能移动的类型,它阻碍了编译器生成默认实现。
不需要其他特殊的成员函数,尽管它们可能有意义。编译器不会为您生成移动构造函数和移动赋值运算符(因为您发布了自己的 copy/assignment 函数),但在您的情况下,您可以轻松地 = default;
它们。析构函数同样可以用 = default;
定义,这与 core guidelines.
一致
请注意,通过 = default
定义析构函数应该在翻译单元中完成,因为 std::unique_ptr
需要在释放其资源时知道完整类型。
是否需要默认构造函数完全取决于你想如何使用 class B
.
正如@lubgr 在他的回答中提到的,您应该 return unique_ptr
而不是来自 clone
函数的原始数据。无论如何,转到您的问题:
B 中需要复制构造函数吗?好吧,这取决于您的用例,但是如果您复制 class 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;
};
- 这又取决于您的需要。我个人也建议重载移动构造函数,以使其使用更少的动态分配(但这可能是万恶之源的过早优化)。
当我需要多态行为时,我经常发现自己在 C++ 中使用唯一指针。我通常实现如下所示的纯抽象 classes:
class A {
public:
virtual A* clone() const = 0; // returns a pointer to a deep copy of A
// other methods go here
};
当我想用它自己的 A 实例修饰另一个 class 时,克隆方法就派上用场了,例如:
#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 给出了关于尝试引用已删除函数的模糊消息),因为唯一指针,这是完全有意义的。我的问题可以总结如下:
我真的需要 B 中的复制构造函数吗?也许有一个更好的模式可以让我完全避免它。
如果 1 是,我可以就此打住吗?我是否需要实现其他默认功能? IE。有没有我还需要默认构造函数和析构函数的场景?
实际上,每当我觉得需要实现默认函数时,我通常会在其他三个函数的旁边实现一个移动构造函数;我通常使用 copy-and-swap-idiom(根据 GManNickG's answer in this thread)。我认为这不会改变任何东西,但也许我错了!
非常感谢!
首先,我认为你的克隆函数的签名可以是
virtual std::unique_ptr<A> clone() = 0;
因为您想要 A
个实例的深层副本和 B
中的独占所有权。其次,当您希望 class 可复制时,您确实必须为它定义一个复制构造函数。赋值运算符也是如此。这是因为 std::unique_ptr
是一个只能移动的类型,它阻碍了编译器生成默认实现。
不需要其他特殊的成员函数,尽管它们可能有意义。编译器不会为您生成移动构造函数和移动赋值运算符(因为您发布了自己的 copy/assignment 函数),但在您的情况下,您可以轻松地 = default;
它们。析构函数同样可以用 = default;
定义,这与 core guidelines.
请注意,通过 = default
定义析构函数应该在翻译单元中完成,因为 std::unique_ptr
需要在释放其资源时知道完整类型。
是否需要默认构造函数完全取决于你想如何使用 class B
.
正如@lubgr 在他的回答中提到的,您应该 return unique_ptr
而不是来自 clone
函数的原始数据。无论如何,转到您的问题:
B 中需要复制构造函数吗?好吧,这取决于您的用例,但是如果您复制 class
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; };
- 这又取决于您的需要。我个人也建议重载移动构造函数,以使其使用更少的动态分配(但这可能是万恶之源的过早优化)。