【问题标题】:C++ copy constructor and shallow copyC++ 拷贝构造函数和浅拷贝
【发布时间】:2010-03-24 14:10:19
【问题描述】:

假设我有一个包含许多显式(静态分配)成员和少量动态分配指针的类。

当我在女巫中声明一个复制构造函数时,我制作了手动分配成员的深层副本,我不想显式复制每个静态分配的成员。

如何在显式复制构造函数中使用隐式(默认)复制构造函数功能?

【问题讨论】:

  • 显式复制构造函数是指用户定义的,而不是显式的关键字。
  • 您可能不是指“静态分配”的成员;你不会复制静态成员。我认为您只是指非指针成员。您可以将这些称为“自动”分配,但即使这样也有点用词不当,因为即使是指针也是自动分配的(当然不一定是它们指向的)。

标签: c++


【解决方案1】:

使用遏制

class outer
{
public:

    outer( const outer& other ) :
        members_( other_.members_ ),
        pmember_( deep_copy( other.pmember_ ))
    {}

    // DON'T FORGET ABOUT THESE TOO

    outer& operator=( const outer& );
    ~outer();

private:

    struct inner
    {
        inner( int i, float f ) :
            int_( i ), float_( f )
        {}

        int   int_;
        float float_;
    };

    inner      members_; //< direct members
    something* pmember_; //< indirect member
};

【讨论】:

  • 定义inner(int, float) 构造函数可能是个好主意,因为outer 很可能想要在非复制构造函数的初始化列表中使用它。
  • 我认为通常人们会对您想要管理的资源使用包含,例如通过在std::vectorstd::tr1::shared_ptr 等中“包含”指针 - 结果证明您不会根本需要一个复制构造函数!
  • 哦,好主意,我使用了内部默认的复制构造函数!使用 getter 和 setter,我可以向用户隐藏这个内部类。
【解决方案2】:

" 分配的几个指针 动态”。

危险,威尔罗宾逊!编写一个包含多个原始指针并为其生命周期操作提供强大异常保证的类是非常困难的,当然不值得付出努力。例如,考虑以下情况:

struct MyClass {
    char *a;
    char *b;
    int i;
    float f;
    MyClass(int i, float f): a(0), b(0), i(i), f(f) {
        a = new char[12];
        b = new char[23];
    }
    ~MyClass() {
        delete[] a;
        delete[] b;
    }
    ...
};

现在,假设b 的分配抛出。 MyClass 的析构函数没有被调用,所以分配给a 的内存被泄露了。因此,您需要显式捕获异常,并且通常会通过错误处理使代码混乱。

相反,定义一个只处理一个指针的小助手类:

struct MyPointerHolder {
    char *value;
    MyPointerHolder(int s) : value(new char[s]) {}
    ~MyPointerHolder() { delete[] value; }
    MyPointerHolder(const MyPointerHolder &rhs) {
        // perform deep copy
    }
    MyPointerHolder &operator=(const MyPointerHolder &rhs) {
        // sample implementation - you might be able to do better
        // by for example just copying the bytes from rhs.
        MyPointerHolder tmp(rhs);
        std::swap(value, tmp.value);
    }
};

struct MyClass {
    MyPointerHolder a;
    MyPointerHolder b;
    int i;
    float f;
    MyClass(int i, float f) : a(12), b(23), i(i) f(f) {}
};

现在 MyClass 不需要显式的复制构造函数、析构函数或赋值运算符。默认值很好。如果b 的初始化程序抛出,MyClass 的析构函数仍然不会被调用。但是因为成员a已经被构造了,它的析构函数调用,释放了内存。

因此,除了获得异常安全性之外,您还减少了编写一个巨大的显式副本、为 MyClass 的每个成员编写一些代码、编写几个小的显式副本、为每种类型的指针成员编写一些代码的问题在 MyClass 中,可以在其他持有相同深拷贝的指针的类中重用。

最后,每次你来编写这些小助手类时,问问自己是否有一个标准库类可以为你做同样的工作。在我的示例中,有两个强大的候选者:std::stringstd::vector&lt;char&gt;。还有各种可能适合的copy_ptrclone_ptr 的提议实现。

【讨论】:

  • 如果您不在某处的成员中记录所持有数组的大小,MyPointerHolder 的复制构造函数将如何工作?
  • 嗯,非标准呼叫msize? NUL-终止数组的内容?使其成为模板参数而不是构造函数参数?你是对的,因为编写的代码需要更改才能完成。不过,这只是一个例子。 MyPointerHolder 通常应该采用它需要的任何构造函数参数来执行深层复制。
  • MyPointerHolderstd::vector 有什么优势?
  • "每次你来写一个小助手类时,问问自己是否有一个标准库类可以为你做同样的工作。在我的例子中,有两个强有力的候选人:@987654335 @ 和 std::vector&lt;char&gt;"。我没有为动态分配的数据指定深拷贝所需的行为,std::vector 可能没有做正确的事情。
  • @Steve:抱歉,我好像没有通读到底。​​span>
【解决方案3】:

你不能。只需在初始化列表中一一分配成员即可。

【讨论】:

  • 我很担心,当我添加一些成员时,我必须记得更改副本 c'tor :(
  • @bartek:是的。只要把它当作一种习惯,你就会自然而然地做到这一点。
【解决方案4】:

可以通过一些间接的方式完成...我来了:)

它基于boost::shared_ptr 的实现,如果我们实际上将两个内存块粘合在一起,而不是持有指向内存的指针,则可以从良好的加速中受益……但随之而来的是对齐问题等等......所以我不会不顾一切地去做。

首先,我们需要一个用于管理内存的类,并在必要时提供自定义解除分配器。

它是间接的,这就是魔法所在。

请注意,它实现了深度复制行为。

namespace detail
{
  // The interface
  template <class T>
  class MemoryOwnerBase
  {
  public:
    virtual ~MemoryOwnerBase() { this->dispose(mItem); mItem = 0; }

    virtual void dispose(T* item) = 0;
    virtual void clone() const = 0;

    T* get() { return mItem; }
    T* release() { T* tmp = mItem; mItem = 0; return tmp; }

    void reset(T* item = 0)
    { 
      if (mItem && item != mItem) this->dispose(mItem);
      mItem = item;
    }

  protected:
    explicit MemoryOwnerBase(T* i = 0): mItem(i) {}
    MemoryOwnerBase(const MemoryOwnerBase& rhs): mItem(0)
    {
      if (rhs.mItem) mItem = new_clone(*rhs.mItem); // Boost Clonable concept
    }
    MemoryOwnerBase& operator=(const MemoryOwnerBase& rhs)
    {
      MemoryOwnerBase tmp(rhs);
      this->swap(rhs);
      return *this;
    }

  private:
    T* mItem;
  };

  // by default, call delete
  template <class T>
  struct DefaultDisposer
  {
    void dispose(T* item) { delete item; }
  };

  // the real class, the type of the disposer is erased from the point of view
  // of its creator
  template <class T, class D = DefaultDisposer<T> >
  class MemoryOwner: public MemoryOwnerBase, private D // EBO
  {
  public:
    MemoryOwner(): MemoryOwnerBase(0), D() {}
    explicit MemoryOwner(T* item): MemoryOwnerBase(item), D() {}
    MemoryOwner(T* item, D disposer): MemoryOwnerBase(item), D(disposer) {}

    virtual void dispose(T* item) { ((D&)*this).dispose(item); }
    virtual MemoryOwner* clone() const { return new MemoryOwner(*this); }
  };

  // easier with type detection
  template <class T>
  MemoryOwnerBase<T>* make_owner(T* item)
  {
    return new MemoryOwner<T>(item);
  }

  template <class T, class D>
  MemoryOwnerBase<T>* make_owner(T* item, D d)
  {
    return new MemoryOwner<T,D>(item,d);
  }
} // namespace detail

然后我们可以制作我们的 Pimpl 类,因为它是你所追求的。

template <class T>
class Pimpl
{
  typedef detail::MemoryOwnerBase<T> owner_base;
public:
  Pimpl(): mItem(0), mOwner(0) {}

  explicit Pimpl(T* item):
    mItem(item), mOwner(item == 0 ? 0 : detail::make_owner(item)) {}

  template <class D>
  Pimpl(T* item, D d):
    mItem(item), mOwner(item == 0 ? 0 : detail::make_owner(item, d)) {}

  Pimpl(const Pimpl& rhs): mItem(), mOwner()
  {
    if (rhs.mOwner)
    {
      mOwner = rhs.mOwner.clone();
      mItem = mOwner->get();
    }
  } 

  T* get() { return mItem; }
  const T* get() const { return mItem; }

  void reset(T* item = 0)
  {
    if (item && !mOwner)
      mOwner = detail::make_owner(item);

    if (mOwner)
    {
      mOwner->reset(item);
      mItem = mOwner->get();
    }
  }

  template <class D>
  void reset(T* item, D d)
  {
    if (mOwner)
    {
      if (mItem == item) mOwner->release();
      delete mOwner;
    }
    mOwner = detail::make_owner(item, d);
    mItem = item;
  }

  T* operator->() { return mItem; }
  const T* operator->() const { return mItem; }

  T& operator*() { return *mItem; }
  const T& operator*() const { return *mItem; }

private:
  T* mItem;                            // Proxy for faster memory access
  detail::MemoryOwnerBase<T>* mOwner;  // Memory owner
}; // class Pimpl

好的,pfiou!

让我们现在使用它:)

// myClass.h
class MyClass
{
public:
  MyClass();

private:
  struct Impl;
  Pimpl<Impl> mImpl;
};

// myClass.cpp
struct MyClass::Impl
{
  Impl(): mA(0), mB(0) {}
  int mA;
  int mB;
};

// Choice 1
// Easy
MyClass::MyClass(): mImpl(new Impl()) {}

// Choice 2
// Special purpose allocator (pool ?)
struct MyAllocatorDeleter
{
  void dispose(Impl* item) { /* my own routine */ }
};

MyClass::MyClass(): mImpl(new Impl(), MyAllocatorDeleter()) {}

是的,它很神奇;)

背后的原理是调用Type Erasure。该机制确保一旦构建了MemoryOwner 对象,它就知道如何删除它持有的内存,并通过间接方式向调用者隐藏确切的机制。

因此您可以将Pimpl&lt;T&gt; 对象视为一个值:

  • DeepCopy 语义
  • DeepConst 语义(volatile 被忽略...)
  • 默认的 CopyConstructor、赋值运算符和析构函数都可以

但要注意它隐藏了一个指针,您的职责是在取消引用它之前确保它是非空的。

如果去掉mOwner参数的惰性初始化,代码可以大大简化。此外,一些例外安全约束:disposer 复制构造函数应该是禁止抛出的,否则所有的赌注都没有。

编辑

解释。

这里的问题是代码绝缘。无论指向的类型如何,都可以对指针执行许多操作,但是对于创建或销毁,我们需要知道底层类型。

4 种基本方法需要创建和销毁,因此需要了解底层类型:

  • 构造函数
  • 复制构造函数
  • 赋值运算符(旧值的破坏)
  • 析构函数

它们本身是实现价值语义所必需的。

C++ 中,有一个称为type erasure 的习语,它包含在虚拟接口后面嵌入类型信息。因此设计的第一部分:

template <class T> class MemoryOwnerBase {};
template <class T, class D> class MemoryOwner: public MemoryOwnerBase<T> {};

MemoryOwnerBase 提供我们正在寻找的基本操作(构造、深拷贝和销毁),并隐藏类型特定信息(如何正确删除)。

MemoryOwner 实现了MemoryOwnerBasevirtual 方法,并通过其D(用于处置器)参数封装了销毁指针所需的知识。

现在,为了操作MemoryOwnerBase,我们需要一个指向它的指针/引用,它没有值语义,因此我们将它包装在Pimpl 类中(代表pointer-to-实现)具有适当的值语义。

请注意,只有 a disposer(用于销毁)需要包装,因为用户需要自己提供一个指针,从而使用 new 运算符。

一个改进是提供一个Pimpl&lt;T&gt; make_pimpl&lt;T,D&gt;(const T&amp;, const D&amp;) 方法来查看内存分配等...但由于上述存储对齐问题,我还没有开始。

【讨论】:

  • Matthieu,我不明白你的代码中的一行,我会在明天早上试一试:),你能添加更多的 cmets 吗?克隆方法中有一个错字 - 我认为它是纯虚拟的,但同样,为什么?如果您有时间请给我们更多提示,您的解决方案看起来值得尝试理解。谢谢。
【解决方案5】:

如果您的结构包含多个指针,您将在正确执行时遇到问题。除非您实际上是在创建智能指针,否则您应该将所有指针包装在对象内(这样您实际上就没有任何指针)。

// 在没有智能指针的情况下执行此操作。很难。
让我演示一下(我可能会弄错,因为这很难)

class D { /* Has a constructor and Destructor */ };
class A
{
    int     a;
    D*      b;
    D*      c;

    public:
        // Default constructor and destructor are easy
        A()
          :a(1)
          ,b(new D)
          ,c(new D[10])
        {}
        ~A()
        {
           delete b;
           delete [] c;
        }
        // Standard copy and swap for assignment.
        // Which pushes all the real work into one place
        A& operator=(A const& copy)
        {
            A  tmp(copy);
            tmp.swap(*this);
            return *this;
        }
        void swap(A& s) throws()
        {
            std::swap(a,s.a);
            std::swap(b,s.b);
            std::swap(c,s.c);
        }
        // OK the hard part getting the copy constructor correct

        A(A const& copy)
        {
            a = copy.a;
            b = new D(copy.b);  // may throw but at this point we don' care
                                // if new throws memory is de-allocated.
                                // If a D constructor throws then all fully constructed members are
                                // destroyed before the memory is de-allocated

            try
            {
                c = new D[10];  // If this throws we do care.
                                // As B needs to be deleted to prevent memory leak
                                // So this whole part needs to be put in a try catch block
                try
                {
                    // This part needs to be in its own try catch block
                    // If the copy constructor throws then  we need to delete c
                    // Note this needs to be seporate to the allocation
                    // As a throw from the call to new that throws will not set c thus calling
                    // delete on it will generate undefined behavior.

                    for(int loop=0;loop < 10;++loop)
                    {
                         std::copy(&copy.c[0],&copy.c[10],c);
                    }
                }
                catch(...)
                {
                    delete [] c;
                    throw;
                }
            }
            catch(...)
            {
                delete b;
                throw;
            }
        }
};

在一个类中只使用 1 个指针更容易,但通常仍然不值得努力。
您的类包含的指针越多,复制构造函数将变得越复杂(正确执行)。结果,将您的指针包装在一个适当的包装器中,该包装器完成所有工作以使代码对那个指针正确。记住每个类最多 1 个指针。

在上述情况下,像动态分配这样的数组的包装器是 std::vector,而单个对象 std::auto_ptr 可以工作,我们可以将上面的代码简化为:

class D { /* Has a constructor and Destructor */ };
template<typename T>
class DeepCpyAPtr  // I am surprised I did not find anything like this in boost
{                  // mybe I am over thinking this and will regret it
    std::auto_ptr<T>   data;
    public:
        explicit DeepCpyAPtr(T* d = NULL)    :data(d)                     {}
        // Other constructors as required
        DeepCpyAPtr(DeepCpyAPtr const& copy) :data(new D(copy.data.get())){}
        DeepCpyAPtr& operator=(DeepCpyAPtr const& copy)
        {
            DeepCpyAPtr  t(copy);
            t.data.swap(data);
            return *this;
        }
        // Destructor Magical

        // Add all the methods you need from std::auto_ptr here.
        T*   get()                {return data.get();}
        T&   operator*()          {return data.operator*();}
        T*   operator->()         {return data.operator->();}
        void reset(T* d = NULL)   {data.reset(d);}
};
class A
{
    int                     a;
    DeepCpyAPtr<D>          b;
    std::vector<D>          c;

    public:
        // Default constructor is easy
        A()
          :a(1)
          ,b(new D)
          ,c(10)
        {}
     // All the compiler generated versions work perfectly now.
};

由于 std::auto_ptr 没有完全正确的语义。我实际上已经为内部使用 auto_ptr 的单一版本编写了一个包装器。但是这个简单的单个附加类(加上向量)使得 A 的实现变得微不足道。

【讨论】:

  • 一个更简单的解决方案可能是将指针包装到它们自己的类中。然后这些可以用作数据成员或私有基类。方便的是,这样的包装已经存在。他们被称为std::vector&lt;D&gt;
  • @sbi:这就是我的观点。手动操作太难了,以至于你的类中不应该有指针。
  • @sbi:添加了一个在没有内部指针的情况下执行此操作的示例,以显示使用适当的包装器时获得正确的容易程度。
【解决方案6】:

我会在一个结构下声明动态分配的成员,然后 memcpy() 他们;

#define _WIN32_WINNT 0x0400
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

class MyClass
{
private:
    static int some_static_member;

public:
    struct DynamicallyAllocated
    {
        int x, y, z;
    } *dyn;

public:
    MyClass(MyClass* copy = NULL)
    {
        this->dyn = NULL;

        if(copy != NULL)
        {
            if(copy->dyn != NULL)
            {
                this->dyn = new DynamicallyAllocated();
                memcpy(this->dyn, copy->dyn, sizeof(*this->dyn));
            }
        }
    }
};

int MyClass::some_static_member = 0;

void main()
{
    MyClass mc1(NULL);
    mc1.dyn = new MyClass::DynamicallyAllocated();
    mc1.dyn->x = 1;
    mc1.dyn->y = 2;
    mc1.dyn->z = 3;
    MyClass mc2(&mc1);
}

您必须将结构内的成员“分组”,因此在使用 memcpy() 时,您不会覆盖某些 C++“其他数据”,例如虚函数的指针。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-09
    • 2013-03-19
    • 1970-01-01
    • 1970-01-01
    • 2012-04-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多