如何为具有unique_ptr
成员变量的类实现复制构造函数?我只考虑 C++11。
6 回答
由于unique_ptr
无法共享,您需要深度复制其内容或将其转换unique_ptr
为shared_ptr
.
class A
{
std::unique_ptr< int > up_;
public:
A( int i ) : up_( new int( i ) ) {}
A( const A& a ) : up_( new int( *a.up_ ) ) {}
};
int main()
{
A a( 42 );
A b = a;
}
正如 NPE 所提到的,您可以使用 move-ctor 而不是 copy-ctor,但这会导致类的不同语义。move-ctor 需要通过以下方式使成员显式可移动std::move
:
A( A&& a ) : up_( std::move( a.up_ ) ) {}
拥有一整套必要的运算符也会导致
A& operator=( const A& a )
{
up_.reset( new int( *a.up_ ) );
return *this,
}
A& operator=( A&& a )
{
up_ = std::move( a.up_ );
return *this,
}
如果你想在 a 中使用你的类std::vector
,你基本上必须决定向量是否应该是对象的唯一所有者,在这种情况下,使类可移动但不可复制就足够了。如果您省略了 copy-ctor 和 copy-assignment,编译器将指导您如何将 std::vector 与仅移动类型一起使用。
在类中拥有 a 的通常情况unique_ptr
是能够使用继承(否则普通对象通常也会这样做,请参阅 RAII)。对于这种情况,到目前为止,该线程中没有适当的答案。
所以,这里是起点:
struct Base
{
//some stuff
};
struct Derived : public Base
{
//some stuff
};
struct Foo
{
std::unique_ptr<Base> ptr; //points to Derived or some other derived class
};
...正如所说,目标是使Foo
可复制的。
为此,需要对包含的指针进行深度复制,以确保正确复制派生类。
这可以通过添加以下代码来完成:
struct Base
{
//some stuff
auto clone() const { return std::unique_ptr<Base>(clone_impl()); }
protected:
virtual Base* clone_impl() const = 0;
};
struct Derived : public Base
{
//some stuff
protected:
virtual Derived* clone_impl() const override { return new Derived(*this); };
};
struct Foo
{
std::unique_ptr<Base> ptr; //points to Derived or some other derived class
//rule of five
~Foo() = default;
Foo(Foo const& other) : ptr(other.ptr->clone()) {}
Foo(Foo && other) = default;
Foo& operator=(Foo const& other) { ptr = other.ptr->clone(); return *this; }
Foo& operator=(Foo && other) = default;
};
这里基本上有两件事:
首先是添加复制和移动构造函数,它们在
Foo
复制构造函数unique_ptr
被删除时被隐式删除。移动构造函数可以简单地通过= default
... 添加,这只是为了让编译器知道不应删除通常的移动构造函数(这有效,因为unique_ptr
已经有一个可以在这种情况下使用的移动构造函数)。对于 的复制构造函数
Foo
,没有类似的机制,因为没有 的复制构造函数unique_ptr
。因此,必须构造一个 newunique_ptr
,用原始指针的副本填充它,并将其用作复制类的成员。如果涉及继承,必须小心地复制原始指针。原因是在上面的代码中进行简单的复制
std::unique_ptr<Base>(*ptr)
会导致切片,即只有对象的基本组件被复制,而派生部分丢失。为避免这种情况,必须通过克隆模式进行复制。这个想法是通过一个在基类
clone_impl()
中返回 a的虚函数来进行复制。Base*
然而,在派生类中,它通过协变进行扩展以返回 aDerived*
,并且这个指针指向派生类的新创建的副本。然后,基类可以通过基类指针访问这个新对象Base*
,将其包装到 a中,并通过从外部调用unique_ptr
的实际函数返回它。clone()
尝试使用此助手创建深层副本,并在源 unique_ptr 为空时应对。
template< class T >
std::unique_ptr<T> copy_unique(const std::unique_ptr<T>& source)
{
return source ? std::make_unique<T>(*source) : nullptr;
}
例如:
class My
{
My( const My& rhs )
: member( copy_unique(rhs.member) )
{
}
// ... other methods
private:
std::unique_ptr<SomeType> member;
};
Daniel Frey 提到了复制解决方案,我会谈谈如何移动 unique_ptr
#include <memory>
class A
{
public:
A() : a_(new int(33)) {}
A(A &&data) : a_(std::move(data.a_))
{
}
A& operator=(A &&data)
{
a_ = std::move(data.a_);
return *this;
}
private:
std::unique_ptr<int> a_;
};
它们被称为移动构造函数和移动赋值
你可以像这样使用它们
int main()
{
A a;
A b(std::move(a)); //this will call move constructor, transfer the resource of a to b
A c;
a = std::move(c); //this will call move assignment, transfer the resource of c to a
}
您需要通过 std::move 包装 a 和 c 因为它们有一个名称 std::move 告诉编译器将值转换为右值引用,无论参数是什么在技术意义上,std::move 类似于“标准::右值"
移动后,unique_ptr的资源被转移到另一个unique_ptr
有许多主题记录了右值引用;这是一个很容易开始的。
编辑 :
移动的对象应保持有效但未指定的状态。
C++ Primer 5, ch13 也很好地解释了如何“移动”对象
我建议使用 make_unique
class A
{
std::unique_ptr< int > up_;
public:
A( int i ) : up_(std::make_unique<int>(i)) {}
A( const A& a ) : up_(std::make_unique<int>(*a.up_)) {};
int main()
{
A a( 42 );
A b = a;
}
unique_ptr
不可复制,只能移动。
这将直接影响测试,即在您的第二个示例中,它也只能移动而不能复制。
事实上,你使用unique_ptr
它来保护你免受大错误是很好的。
例如,您的第一个代码的主要问题是指针永远不会被删除,这非常非常糟糕。说,您可以通过以下方式解决此问题:
class Test
{
int* ptr; // writing this in one line is meh, not sure if even standard C++
Test() : ptr(new int(10)) {}
~Test() {delete ptr;}
};
int main()
{
Test o;
Test t = o;
}
这也很糟糕。如果你复制会发生什么Test
?将有两个类具有指向相同地址的指针。
当一个Test
被销毁时,它也会销毁指针。当你的第二个Test
被销毁时,它也会尝试删除指针后面的内存。但是它已经被删除了,我们会得到一些糟糕的内存访问运行时错误(或者如果我们不走运,会出现未定义的行为)。
所以,正确的做法是要么实现拷贝构造函数,要么实现拷贝赋值运算符,这样行为就清晰了,我们就可以创建一个拷贝了。
unique_ptr
在我们这里遥遥领先。它具有语义:“我是unique
,所以你不能只是复制我。 ”因此,它可以防止我们现在实现手头的运算符的错误。
您可以为特殊行为定义复制构造函数和复制赋值运算符,并且您的代码将起作用。但是你,理所当然地(!),被迫这样做。
故事的寓意:总是unique_ptr
在这种情况下使用。