【问题标题】:Call default copy constructor from within overloaded copy constructor从重载的复制构造函数中调用默认复制构造函数
【发布时间】:2011-09-04 02:17:57
【问题描述】:

我需要编写一个复制构造函数来深度复制std::shared_ptr 的内容。但是,类中还定义了一堆变量int a, b, c, d, e;。有没有办法在我的新重载代码中生成默认的复制构造函数代码(或调用默认的复制构造函数)。

这是一个带有注释的代码 sn-p,希望能澄清问题。

class Foo {
public:
     Foo() {}
     Foo(Foo const & other);
     ...
private:
     int a, b, c, d, e;
     std::shared_ptr<Bla> p;
};

Foo::Foo(Foo const & other) {
    p.reset(new Bla(*other.p));

    // Can I avoid having to write the default copy constructor code below
    a = other.a;
    b = other.b;
    c = other.c;
    d = other.d;
    e = other.e;
}

【问题讨论】:

  • 为什么需要深拷贝一个shared指针?它的全部目的不就是共享一个公共资源吗?如果您拥有唯一所有权,请考虑使用unique_ptr
  • 因为 Foo 的每个实例都需要一个 shared_ptr 来传递给其他几个类。
  • 上述代码中的“...”为p提供了一个公共接口。

标签: c++ constructor shared-ptr copy-constructor deep-copy


【解决方案1】:

我一直认为像这样的问题应该至少有一个来自标准的答案引用给未来的读者,所以就在这里。

该标准的§12.8.4 规定:

如果类定义没有显式声明复制构造函数,则隐式声明一个。

这意味着当一个类定义确实显式声明了一个复制构造函数时,一个没有隐式声明。所以如果显式声明一个,隐式的就不存在了,所以不能调用。

【讨论】:

  • 不,标准中的引用并不意味着这一点。
  • @wilhelm 为什么不呢?如果任何语句的 if 不满足,则 then 不成立。即使这种普遍性是错误的,它的这种特殊应用似乎也是正确的。不过,我以前从来没有错。
  • “如果不是 A 则 B”并不意味着“如果 A 则不是 B”。但我知道你的意思,在这种情况下,“如果 A 则不是 B”也是正确的。
  • @Lex 好吧,我在考虑英语。例如,如果你妈妈对你说“如果你好,我会带你去玩具店”,你可以假设如果你不好,她不会带你去玩具店。
  • 是的,这是你的一个很好的猜测(就妈妈而言),但这不是保证,标准就是定义保证。
【解决方案2】:

这是我写这篇文章时的问题代码:

class Foo {
public:
     Foo() {}
     Foo(Foo const & other);
     ...
private:
     int a, b, c, d, e;
     std::shared_ptr<Bla> p;
};

Foo::Foo(Foo const & other) {
    p.reset(new Bla(other.p));

    // Can I avoid having to write the default copy constructor code below
    a = other.a;
    b = other.b;
    c = other.c;
    d = other.d;
    e = other.e;
}

上面的代码很可能错误,因为

  1. 默认构造函数使abcde未初始化,并且

  2. 代码不负责赋值复制,

  3. 表达式new Bla(other.p) 要求Bla 有一个采用std::shared_ptr&lt;Bla&gt; 的构造函数,这是极不可能的。

对于std::shared_ptr,这必须是 C++11 代码才能在语言方面正式正确。但是,我相信只是代码使用了编译器可用的东西。所以我相信相关的C++标准是C++98,加上C++03修正案的技术修正。

即使在 C++98 中,您也可以轻松利用内置(生成的)副本初始化,例如

namespace detail {
    struct AutoClonedBla {
        std::shared_ptr<Bla> p;

        AutoClonedBla( Bla* pNew ): p( pNew ) {}

        AutoClonedBla( AutoClonedBla const& other )
            : p( new Bla( *other.p ) )
        {}

        void swap( AutoClonedBla& other )
        {
            using std::swap;
            swap( p, other.p );
        }

        AutoClonedBla& operator=( AutoClonedBla other )
        {
            other.swap( *this );
            return *this;
        }
    };
}

class Foo {
public:
     Foo(): a(), b(), c(), d(), e(), autoP( new Bla ) {}
     // Copy constructor generated by compiler, OK.

private:
     int                      a, b, c, d, e;
     detail::AutoClonedBla    autoP;
};

请注意,此代码确实在默认构造函数中正确初始化,确实负责复制分配(为此使用 swap idiom),并且不需要特殊的智能指针感知 Bla 构造函数,而只是使用普通的Bla复制构造函数进行复制。

【讨论】:

  • Bla 复制构造函数不打算采用 shared_ptr。接得好。答案仍然有效,非常好,谢谢。
【解决方案3】:

shared_ptr 上编写一个内置深度复制的变体会更容易。这样,您不必为您的主类编写复制构造函数;只为这种特殊的deep_copy_shared_ptr 类型。您的 deep_copy_shared_ptr 将有一个复制构造函数,它会存储一个 shared_ptr 本身。它甚至可以隐式转换为shared_ptr,使其更易于使用。

【讨论】:

  • +1 用于指出 OP 问题的解决方案。我有点惊讶这个答案有 0 票(在我之前),而不是标记为解决方案的答案。
【解决方案4】:

这是不可能的。要么您编写自定义复制构造函数(完全由您自己编写),要么编译器为您编写。

请注意,如果您编写一个复制构造函数,那么您可能还需要一个复制赋值和一个析构函数,因为编写这三个资源管理函数中的任何一个都意味着您正在管理一个资源。但是,使用复制和交换习语,您只需在复制构造函数中编写一次复制逻辑,然后根据复制构造函数定义赋值运算符。

除此之外,我不完全确定您为什么使用shared_ptr&lt;&gt;shared_ptr&lt;&gt; 的意义在于允许多个指针安全地指向同一个对象。但是您没有共享指针,而是深度复制它。也许您应该改用原始指针,并在析构函数中释放它。或者,更好的是,将shared_ptr&lt;&gt; 替换为clone_ptr,然后完全消除复制构造函数、复制赋值和析构函数。

【讨论】:

  • 析构函数和赋值在这里应该没问题,因为共享指针基本上已经处理了所有事情,但它肯定是一个非常不寻常的设计选择。
  • 他可能至少需要一个赋值运算符,否则赋值给一个对象和复制构造一个对象会做不同的事情——这是违反直觉的。但事实上,这是一个不寻常的案例,以至于我只是把自己弄糊涂了,以为他需要一个析构函数。这表明设计可以做得更好(以“最少惊喜”的名义)。
  • 这里到底有什么不寻常的地方?赋值运算符也在"..." 中定义。这不是问题的重点。关键是不必为int a, b, c, d, e; 重新编写可能已经由编译器生成的复制代码会很好。
  • @LexFridman 会很好,是的,但同样:这是不可能的。您要求编写 一些 的复制逻辑,并将其余部分委托给编译器编写。 C++ 不允许这样做:您要么不说一句话,编译器就会为您生成复制构造函数,或者您明确要求编译器不要生成复制 ctor,编译器不会为您编写任何内容。
  • @LexFridman 不同寻常的是,您的代码实际上并没有真正管理资源,但它依赖于自定义复制逻辑。这很不寻常,您可能会改进此设计。
【解决方案5】:

据我所知,您可以(并且应该)做的就是使用初始化列表:

Foo::Foo(Foo const & other)
    : a(other.a), b(other.b), c(other.c), 
      d(other.d), e(other.e), p(new Bla(other.p))
{}

这不会让你免于写作,但它会让你免于分配(不必要的)默认构造的成员(尽管在这种情况下它可能没问题)以及这可能带来的许多其他陷阱的可能性能损失.如果可能,请始终在构造函数中使用初始化列表。

顺便说一句,Kerrek 的评论是正确的。如果您仍然进行深层复制,为什么需要shared_ptr。在这种情况下,unique_ptr 可能更合适。除了它的好处shared_ptr 不是一个通用的 no-more-to-think-about-deallocation 解决方案,您应该始终考虑是否需要智能指针以及哪种类型的智能指针最合适。

【讨论】:

  • 一般来说,在初始化列表中分配(动态)资源并不是一个好主意。如果你在初始化列表中分配了两个资源,而第二个资源构造失败,那么你有一个无法修复的泄漏(在第一个资源中)。
【解决方案6】:

默认赋值运算符的代码与默认复制构造函数相同。虽然你不能从你的重载中调用默认的复制构造函数,但是同样的代码存在于assignemnt中。

Foo::Foo(Foo const & other) 
{
    *this = other;
    p.reset(new Bla(other.p));
}

应该会给你你需要的东西。

编辑:没关系,它实际上是相同的代码,并且显式声明复制构造函数会阻止它的生成:(

【讨论】:

  • 赋值运算符对p做了什么?
  • 默认赋值运算符只是默认调用所有成员的默认赋值运算符。所以对于 p 它相当于调用 p = other.p p 是一个 shared_ptr 所以它获取值并增加引用计数。假设上面的代码可以工作,p.reset 将减少原始对象的引用计数。
【解决方案7】:

一个技巧可以是将这些“简单”字段放入基类中

class FooBase {
protected:
    int a, b, c, d, e;
};

class Foo : public FooBase {
public:
     Foo() {}
     Foo(const Foo& other)
       : FooBase(other),         // This copies all the simple fields
         p(new Bla(other.p))     // Manually do the hard part
     {}
     ...
private:
     std::shared_ptr<Bla> p;
};

然后,如果你在 Foo 中添加任何简单的字段,你可以将它们放到 FooBase 中,而无需担心更新复制构造函数。

如果需要,不要忘记添加类似的复制赋值运算符。在现代编译器上,还添加了移动构造函数和移动赋值运算符。这两个可能是默认的,因为您不需要在移动操作时进行深拷贝。

【讨论】:

    猜你喜欢
    • 2020-05-14
    • 2017-07-28
    • 2016-03-25
    • 1970-01-01
    • 2014-03-24
    • 1970-01-01
    • 2015-07-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多