如何使用智能指针作为类属性来复制对象?

8

我从boost库文档中读到:

从概念上讲,智能指针被视为拥有所指向的对象,并因此在不再需要该对象时负责删除它。

我有一个非常简单的问题:我想为类的指针属性使用RAII,该类是可复制和可赋值的。

复制和赋值操作应该是深层的:每个对象都应该拥有自己的实际数据副本。此外,属性需要提供RTTI(它们的类型也可以在运行时确定)。

我应该搜索一个Copyable智能指针的实现(数据很小,所以我不需要写时复制指针),还是像这个答案中所示,将复制操作委托给我的对象的复制构造函数?

我希望您能为可复制和可赋值的类选择一个简单RAII的智能指针?(我认为unique_ptr是一个合适的选择,可以委托类的复制构造函数和赋值运算符。但我不确定)
这里是一个使用原始指针的问题伪代码,只是一个问题描述,不是C++代码的运行:
// Operation interface
class ModelOperation
{
    public: 
        virtual void operate = (); 
};

// Implementation of an operation called Special 
class SpecialModelOperation
:
    public ModelOperation
{
    private:
        // Private attributes are present here in a real implementation. 

    public: 

        // Implement operation
        void operate () {}; 
};

// All operations conform to ModelOperation interface
// These are possible operation names: 
// class MoreSpecialOperation; 
// class DifferentOperation; 

// Concrete model with different operations
class MyModel 
{
    private: 
        ModelOperation* firstOperation_; 
        ModelOperation* secondOperation_;  

    public:

        MyModel()
            : 
                firstOperation_(0), 
                secondOperation_(0)
        {
            // Forgetting about run-time type definition from input files here.
            firstOperation_  = new MoreSpecialOperation(); 
            secondOperation_ = new DifferentOperation(); 
        }

        void operate()
        {
            firstOperation_->operate(); 
            secondOperation_->operate();
        }

        ~MyModel() 
        {
            delete firstOperation_; 
            firstOperation_ = 0; 

            delete secondOperation_; 
            secondOperation_ = 0; 
        }
};

int main()
{

    MyModel modelOne; 

    // Some internal scope
    {
        // I want modelTwo to have its own set of copied, not referenced 
        // operations, and at the same time I need RAII to for the operations, 
        // deleting them automatically as soon as it goes out of scope. 
        // This saves me from writing destructors for different concrete models.  
        MyModel modelTwo (modelOne); 
    }


    return 0;
}

1
@Joachim 是的,但它们共享同一对象的引用。我需要复制操作执行深度复制。 - tmaric
完全不使用智能指针是一个选项吗? - R. Martinho Fernandes
在您所描述的情况下,成员是否需要是指针? - NPE
@R.MartinhoFernandes 我需要在属性上运行RTTI,而且属性的类型可能会在运行时确定。 - tmaric
@tomislav-maric 好的,只是确认一下。不过你可能想把那些信息编辑到问题中。 - R. Martinho Fernandes
显示剩余2条评论
5个回答

6
如果您接受某些类型的要求,这可以在不需要为所有类型实现虚拟克隆函数的情况下完成。具体要求是类型具有可访问的复制构造函数,但由于潜在的意外切片可能会被一些人认为不太理想。然而,适当使用友元可以缓解这种缺点。
如果这样做是可以接受的,可以通过在提供复制功能的接口下消除派生类型来进行:
template <typename Base>
struct clonable {
    // virtual copy
    // this clone function will be generated via templates
    // no boilerplate is involved
    virtual std::unique_ptr<clonable<Base>> clone() const = 0;

    // expose the actual data
    virtual Base* get() = 0;
    virtual Base const* get() const = 0;

    // virtual destructor
    // note that this also obviates the need for a virtual destructor on Base
    // I would probably still make it virtual, though, just in case
    virtual ~clonable() = default;
};

这个接口是由一个基于最终派生类型的类模板实现的,因此通过复制构造函数知道如何进行正常的复制。

template <typename Base, typename Derived>
struct clonable_holder : clonable<Base> {
    // I suppose other constructors could be provided
    // like a forwarding one for emplacing, but I am going for minimal here
    clonable_holder(Derived value)
    : storage(std::move(value)) {}

    // here we know the most derived type, so we can use the copy constructor
    // without risk of slicing
    std::unique_ptr<clonable<Base>> clone() const override {
        return std::unique_ptr<clonable<Base>>(new clonable_holder(storage));
    }

    Base* get() override { return &storage; }
    Base const* get() const override { return &storage; }

private:
    Derived storage;
};

这将为我们生成虚拟副本函数,无需额外的样板文件。现在我们可以在此基础上构建类似智能指针的类(不完全是智能指针,因为它并没有提供指针语义,而是提供值语义)。
template <typename Base>
struct polymorphic_value {
    // this constructor captures the most derived type and erases it
    // this is a point where slicing may still occur
    // so making it explicit may be desirable
    // we could force constructions through a forwarding factory class for extra safety
    template <typename Derived>
    polymorphic_value(Derived value)
    : handle(new clonable_holder<Base, Derived>(std::move(value))) {
        static_assert(std::is_base_of<Base, Derived>::value,
            "value must be derived from Base");
    }

    // moving is free thanks to unique_ptr
    polymorphic_value(polymorphic_value&&) = default;
    polymorphic_value& operator=(polymorphic_value&&) = default;

    // copying uses our virtual interface
    polymorphic_value(polymorphic_value const& that)
    : handle(that.handle->clone()) {}
    polymorphic_value& operator=(polymorphic_value const& that) {
        handle = that.handle->clone();
        return *this;
    }

    // other useful constructors involve upcasting and so on

    // and then useful stuff for actually using the value
    Base* operator->() { return handle.get(); }
    Base const* operator->() const { return handle.get(); }
    // ...

private:
    std::unique_ptr<clonable<Base>> handle;
};

这只是一个最基本的界面,但可以从这里轻松扩展以涵盖更多的使用场景。

非常感谢您提供详细的答案!然而,这似乎比简单地封装一个指针并为其定义复制和赋值更加复杂。也许是因为我的经验不足...但是这似乎是解决问题的一种相当复杂的方法... - tmaric
感谢 @R.MartinhoFernandes; 我喜欢这个方法。但从我的角度来看,阻碍并不是对类型的约束 (所有可复制构造的等等), 而更多地在于进行所有权处理的地方需要知道可能存储的每个最终派生类型的定义。我喜欢这种整洁的方式,它避免了在这些情况下为每个类实现 clone(),以及如何处理有几个最终派生类型 (我认为?)。 - Nicholas Wilson
我在这里一定漏掉了什么。我快速地写了一个关于封装指针的想法,链接在这里:http://pastebin.com/qS27A52v,它似乎正在运行,我运行了valgrind检查,没有内存泄漏,并且封装指针的简单复制操作将复制构造函数委托给模板参数。这种方法有什么问题吗? - tmaric
1
@tomislav-maric 你所忽略的是,除非它是最派生类的构造函数,否则你不能使用复制构造函数。在这段代码中,你使用了两个 wrapPtr<ModelOperation>。这意味着在这一行 t_ = new T (rhs()); 中,类型 T 将是 ModelOperation,这将调用 ModelOperation 构造函数,而不是 SpecialModelOperationDifferentModelOperation 的构造函数。它只是丢失了所有不在基类中的信息。 - R. Martinho Fernandes
1
更明确地说:如果你把一个 SpecialModelOperation 放进那个 wrapPtr<ModelOperation> 中,然后复制它,那份拷贝将有一个 ModelOperation 的实例,而不是 SpecialModelOperation 的实例。 - R. Martinho Fernandes
显示剩余3条评论

5
很抱歉有点晚了,但是对于未来的观众:我的头文件库Aurora中有一个现成的实现,以及它的SmartPtr教程。使用Aurora,通过智能指针实现深拷贝非常容易。以下代码适用于任何可复制类型T:
aurora::CopiedPtr<T> first(new T);
aurora::CopiedPtr<T> second = first; // deep copy

这使得如果你的类具有指针成员,实现“大三/五”通常是不必要的。

3
永远不晚。我读了你的回答,发现你的图书馆正是我正在寻找的。谢谢! - Alex Oliveira

1

我从未听说过现成的实现,但你可以自己简单地完成它。

首先,您应该编写一些模板包装类,其中包含虚拟克隆方法,返回存储对象的副本。然后编写一些多态持有该类的容器,该容器可以被复制。

并且不要忘记进行检查删除http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete


1

听起来好像需要能够制作一个智能指针,每次创建另一个智能指针对象时都会创建一个新的对象副本。(我猜这个副本是否“深度”取决于对象的构造函数;我们所存储的对象可能具有多层所有权,因此“深度”取决于对象的含义。对于我们的目的来说,主要是希望在使用另一个智能指针的引用构造智能指针时创建一个不同的对象,而不仅仅是取出现有对象的指针。)

如果我正确理解了问题,那么您将需要一个虚拟克隆方法。没有其他方法可以正确调用派生类的构造函数。

struct Clonable {
  virtual ~Clonable() {}
  virtual Clonable* clone() = 0;
};
struct AutoPtrClonable {
  AutoPtrClonable(Clonable* cl=0) : obj(cl) { }
  AutoPtrClonable(const AutoPtrClonable& apc) : obj(apc.obj->clone()) { }
  ~AutoPtrClonable() { delete obj; }
  // operator->, operator*, etc
  Clonable* obj;
};

要使用示例代码,将其制作成模板等。


我正在考虑完全放弃智能指针。当涉及到智能指针时,RAII是基于所有权的,但是在std::或boost::中没有可复制的智能指针可用,除非转移所有权或使用引用,这似乎存在冲突。 - tmaric
1
@tomislav,除非你有一个特殊的接口(比如Clonable),否则你必须拥有所有权或使用引用的转移:给定一个指针对象,普通调用例如复制构造函数无法复制该对象。编译器如何知道要调用哪个对象的构造函数——最派生的?这需要虚拟查找,在C++中,你必须编写自己的clone()来实现这一点。 - Nicholas Wilson
1
如果您可以接受一些类型要求,那么您可以不使用克隆函数,而是直接使用常规的复制构造函数来完成此操作。解决方案涉及通过模板外部自动生成克隆函数。(我会尽快写出关于这个问题的答案;可惜我的当前项目编译速度非常快) - R. Martinho Fernandes
@NicholasWilson 如果我使用指针包装,似乎是完成任务的最小工作量。没有智能指针,我自己写了一个小类,可以按照我的需求完成复制工作,并且仍然将指针封装起来,以便使用RAII。 - tmaric

0

你有两种解决方案(其实你还有很多,但这些对我来说最有意义:)

首先,你可以使用std::unique_ptr。这是一个好的解决方案,因为它强制你每个指针只有一个实例。(使用std::shared_ptr 也可以,但如果你不显式添加代码,共享指定对象的复制和赋值就会出现问题,这是你想避免的)。

如果你使用std::unique_ptr,你的拷贝构造函数和赋值运算符应该明确地深度复制(使用虚拟clone方法在点对象的接口中或者在调用unique_ptr构造函数时使用new操作符)。

其次,你可以自己编写。这并不复杂,我们只需编写一个小型(大约10-20行)的实用类。

就我个人而言,如果我必须在一个地方使用这个智能指针类,我将使用std::unique_ptr。否则(多个指针,相同的行为),我会自己编写,这样我就不必为许多实例重复深度复制(以遵循DRY原则)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接