【问题标题】:How to approach copying objects with smart pointers as class attributes?如何使用智能指针作为类属性来复制对象?
【发布时间】:2012-12-04 10:33:09
【问题描述】:

boost library documentation我读到这个:

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

我有一个非常简单的问题:我想将 RAII 用于可复制和可分配类的指针属性。

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

我应该搜索可复制智能指针的实现(数据很小,所以我不需要Copy on Write 指针),还是我应该将复制操作委托给我的对象的复制构造函数,如图所示this answer?

对于可复制和可分配的类的简单 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;
}

【问题讨论】:

  • 如果您阅读 description of shared_ptr,您会发现它们是 CopyConstructible 和 Assignable。
  • @Joachim 是的,但是它们共享对同一对象的引用。我需要复制操作来执行深层复制。
  • 根本不使用任何智能指针?
  • 在您描述的场景中,成员甚至需要是指针吗?
  • @R.MartinhoFernandes 我需要 RTTI 来运行属性,而且属性的类型可以在运行时确定。

标签: c++ smart-pointers copy-constructor assignment-operator


【解决方案1】:

如果您接受对您的类型的某些要求,则无需为所有类型实现虚拟克隆功能即可完成。特殊要求是这些类型具有可访问的复制构造函数,由于可能会意外切片,有些人会认为这是不可取的。不过,正确使用交友功能可能会减轻其缺点。

如果可以接受,可以通过在提供复制功能的接口下擦除派生类型来解决此问题:

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

这只是一个最小的界面,但可以从这里轻松充实以涵盖更多使用场景。

【讨论】:

  • 绝对是一个详细的答案,谢谢!然而,这似乎比包装一个指针并为它定义副本和赋值要复杂得多。可能是我缺乏经验……但这似乎是解决问题的一种相当复杂的方法……
  • 您将如何在“包装指针并为其定义副本和分配”方法中制作副本?将clone 函数添加到ModelOperation 接口并在每个派生类中手动实现?请注意,此代码中的所有代码都将编写一次并重复使用,而不是每个派生类一次。
  • 另请注意,您的示例不是异常安全的:如果secondOperation_ = new DifferentOperation(); 抛出会发生什么?并不是MyModel的析构函数在构造函数没有完成的情况下不会被调用,所以上一行创建的MoreSpecialOperation只会泄漏。使用类似智能指针的类将使其自动安全。所有这一切都没有任何样板:你的例子变成class MyModel { polymorphic_value&lt;ModeOperation&gt; firstOperation; polymorphic_value&lt;ModeOperation&gt; secondOperation; /* no copy ctors, nor assignment ops, nor destructors to write */ };
  • @tomislav-maric 你所缺少的是你不能使用复制构造函数,除非它是最派生的类之一。在该代码中,您使用两个wrapPtr&lt;ModelOperation&gt;。这意味着在这一行t_ = new T (rhs()); 中,T 类型将是ModelOperation,这将调用ModelOperation 构造函数,而不是SpecialModelOperationDifferentModelOperation 构造函数。它只是丢失了所有不在基础中的信息。
  • 更明确地说:如果您在 wrapPtr&lt;ModelOperation&gt; 中放入 SpecialModelOperation 然后复制它,则该副本将具有 ModelOperation 的实例,而不是 SpecialModelOperation 的实例。
【解决方案2】:

有点晚了,但对于未来的观众:在我的仅标头库Aurora 及其SmartPtr tutorial 中有一个现成的实现。使用 Aurora,通过智能指针实现深度复制非常简单。以下代码适用于任何可复制类型T

aurora::CopiedPtr<T> first(new T);
aurora::CopiedPtr<T> second = first; // deep copy

如果你的类有指针成员,这通常不需要实现三巨头/五巨头。

【讨论】:

  • 永远不会太晚。我读了你的回答,发现你的图书馆正是我想要的。谢谢!
【解决方案3】:

听起来需要能够创建一个智能指针,每次创建另一个智能指针对象时都会创建一个对象的新副本。 (我猜该副本是否“深”取决于对象的构造函数;据我们所知,您存储的对象可能具有许多深层次的所有权,因此“深”取决于对象。我们的主要目的是,当智能指针使用来自另一个对象的引用构造时,您想要创建一个不同对象的东西,而不是仅仅取出一个指向现有对象的指针。)

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

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:: 中没有可复制的智能指针这一事实之间似乎存在冲突。
  • @tomislav 除非您有特殊的接口(如 Clonable),否则您必须转让所有权或使用引用:通过指针给定对象,例如复制构造函数的普通调用不能复制物体。编译器如何知道要调用哪个对象的构造函数——派生最多的?这需要一个虚拟查找,而在 C++ 中,您必须编写自己的 clone() 来获得它。
  • 如果你能忍受对你的类型的一些要求,你可以在没有克隆函数的情况下做到这一点,只需使用常规的复制构造函数。该解决方案涉及通过模板在外部自动生成克隆功能。 (我会在有空的时候看看能不能写一个答案;遗憾的是,我当前的项目编译速度非常快)
  • @NicholasWilson 如果我包装一个指针,这似乎是完成任务的最小工作量。没有智能指针,我自己的小类,按照我的需要完成复制工作,并且仍然打包指针以便使用 RAII。
【解决方案4】:

我从未听说过即用型实现,但您可以自己完成。

首先你应该编写一些模板包装类,它具有虚拟克隆方法,返回存储对象的副本。然后写一些可以复制的类的多态持有者

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

【讨论】:

    【解决方案5】:

    你有两个解决方案(实际上你有更多,但这些对我来说最有意义:)):

    首先,您可以使用std::unique_ptr。这是一个很好的解决方案,因为它强制您每个指针都有一个实例。 (使用std::shared_ptr 也可以,但如果您不显式添加代码,shared_ptr 的复制和分配将“共享”——尤其是您想要避免的)。

    如果您使用std::unique_ptr,您的复制构造函数和赋值运算符应该显式地进行深度复制(使用指针接口中的虚拟clone 方法,或调用unique_ptr 构造函数中的new 运算符)。

    其次,您可以自己滚动。它没有什么复杂的,我们谈论的是一个小型(10-20 行左右)实用程序类。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-02-19
      • 2016-03-18
      • 1970-01-01
      • 1970-01-01
      • 2018-08-31
      • 1970-01-01
      • 2013-03-16
      • 1970-01-01
      相关资源
      最近更新 更多