【问题标题】:Is this good code? (copy constructor and assignment operator )这是好代码吗? (复制构造函数和赋值运算符)
【发布时间】:2009-09-22 02:20:33
【问题描述】:

出于某种原因,我不得不为我的类同时提供一个复制构造函数和一个 operator=。如果我定义了一个复制 ctor,我想我不需要 operator=,但 QList 想要一个。抛开这些,我讨厌代码重复,那么这样做有什么问题吗?

Fixture::Fixture(const Fixture& f) {
    *this = f;
}

Fixture& Fixture::operator=(const Fixture& f) {
    m_shape         = f.m_shape;
    m_friction      = f.m_friction;
    m_restitution   = f.m_restitution;
    m_density       = f.m_density;
    m_isSensor      = f.m_isSensor;
    return *this;
}

只是出于好奇,没有办法切换它,以便大部分代码在复制 ctor 中,operator= 以某种方式利用它?我试过return Fixture(f);,但它不喜欢那样。


看来我需要更清楚地说明,复制构造函数和赋值运算符已被我继承的类隐式禁用。为什么?因为它是一个不应该单独实例化的抽象基类。然而,这个类是独立的。

【问题讨论】:

  • 您的新代码无休止地递归。它只是再次调用operator=。不好。
  • @litb:哦……哈哈。哎呀:$
  • 别担心!我的第一个答案当然也包含同样的问题,只是被隐藏在一个 std::swap 调用中。史诗般的失败 xD
  • 您是否注意到所选答案的票数最少。您意识到这是有原因的!
  • @Martin:我认为原因是因为当我将其标记为答案时它仍然是一个新答案。从那以后已经有好几个小时了,而且似乎不太受欢迎。我想我将对此进行更多探索。

标签: c++ operator-overloading copy-constructor


【解决方案1】:

这很糟糕,因为operator= 不能再依赖设置对象了。你应该反过来做,并且可以使用复制交换习语。

如果您只需要复制所有元素,则可以使用隐式生成的赋值运算符。

在其他情况下,您将不得不做一些额外的事情,主要是释放和复制内存。这就是复制交换习语的用武之地。它不仅优雅,而且还提供了这样的功能,如果它只交换原语,它就不会抛出异常。让我们指向一个需要复制的缓冲区的类:

Fixture::Fixture():m_data(), m_size() { }

Fixture::Fixture(const Fixture& f) {
    m_data = new item[f.size()];
    m_size = f.size();
    std::copy(f.data(), f.data() + f.size(), m_data);
}

Fixture::~Fixture() { delete[] m_data; }

// note: the parameter is already the copy we would
// need to create anyway. 
Fixture& Fixture::operator=(Fixture f) {
    this->swap(f);
    return *this;
}

// efficient swap - exchanging pointers. 
void Fixture::swap(Fixture &f) {
    using std::swap;
    swap(m_data, f.m_data);
    swap(m_size, f.m_size);
}

// keep this in Fixture's namespace. Code doing swap(a, b)
// on two Fixtures will end up calling it. 
void swap(Fixture &a, Fixture &b) {
  a.swap(b);
}

这就是我通常写赋值运算符的方式。阅读Want speed? Pass by value 了解异常赋值运算符签名(按值传递)。

【讨论】:

  • 听起来不错,但为什么我需要交换它们呢?谁在乎 f 的值是多少?这不是不必要的抄袭吗?更不用说我复制 f 只是为了调用该函数,因为您删除了引用...
  • 在你交换之前,你想要的值在f,但是你想要它们在*this。这就是你交换的原因。如果您将 f 保留为 const 引用,那么您必须在正文中执行 Fixture c = f; swap(*this, c); - 所以无论如何您都必须复制。最好对调用者尽可能透明地进行复制。阅读cpp-next.com/archive/2009/08/want-speed-pass-by-value
  • 是的,我想要*this 中来自f 的值。但我不需要将*this 复制到f。肯定有分配,但这会导致分配两倍,不是吗?
  • 我觉得越来越优雅了:O
  • 等等 - 我们真的忽略了一些事情:你需要交换,因为你希望你的旧日期被释放。如果您只是分配,则不会释放旧数据。如果你只是交换指针,而不是其他数据,那么在参数的析构函数中(它将保存你的旧指针,但还有一些其他数据),你会得到不一致。所以你必须经常交换。
【解决方案2】:

复制 ctor 和赋值是完全不同的——赋值通常需要释放它正在替换的对象中的资源,复制 ctor 正在处理一个尚未初始化的对象。由于在这里您显然没有特殊要求(分配时不需要“释放”),因此您的方法很好。更一般地说,您可能有一个“释放对象持有的所有资源”辅助方法(在 dtor 和分配开始时调用)以及“将这些其他东西复制到对象中”部分相当接近典型的抄写员的工作(或者大部分,无论如何;-)。

【讨论】:

  • 假设我确实需要释放一些资源,那么这种方法根本行不通吧?因为那时我会尝试释放复制 ctor 中未初始化的资源(因为它调用 operator=)。这导致了我遇到的最后一个问题——无论如何要交换它以便operator= 调用复制构造函数?这样我就可以释放资源,然后复制数据。
  • 最好的可能是拥有“free stuff”和“copy stuff”辅助方法并调用它们来构建copy ctor、assignment和dtor——不像litb的std::swap想法那样通用,但如果分配和复制构建对于您所拥有的资源来说是等效的,那么会便宜一些。
  • +1:在阅读了 litb 的答案和其他方法后,我决定最喜欢你的方法。是的,它看起来不是很优雅,但它结合了无代码冗余和最佳性能。 (另请参阅我对 litb 和 Goz 的回答的评论)
  • 我偷偷地怀疑,如果赋值和复制构造是等价的(即没有可释放的资源),那么默认的复制构造函数和 operator= 就可以了,你应该根本不用写任何代码。但我无法证明这一点,无论如何默认 operator= 和这种方法都有异常安全问题,只有 copy-and-nothrow-swap 可以解决,除非你知道你所有的成员都有 nothrow operator=。如果你使用异常,那么copy-and-swap 与 RAII 大致相同,是 C++ 的基本习语。
  • ...如果复制失败,将 operator= 实现为“免费,然后复制”的问题在于,您最多只能提供弱异常保证,即使是这样,您也必须抓住任何异常并撤消您对“副本”所做的操作,以便将分配给的对象清除为“空”状态。这既不是它的初始状态,也不是期望的状态,因此没有强有力的保证。
【解决方案3】:

你只是在你的例子中做一个成员明智的复制和分配。这不是你需要自己写的东西。编译器可以生成完全执行此操作的隐式复制和赋值操作。如果编译器生成的不合适(即,如果您通过指针或类似的东西管理某些资源),您只需要编写自己的复制构造函数、赋值和/或析构函数

【讨论】:

  • 好点。这也是我对示例代码的看法。不知道为什么它需要自己的复制赋值运算符。
  • 不要粗鲁,但请阅读第一句话 :) 我继承自另一个隐式禁用复制构造函数和赋值运算符的类。我需要在我自己的类中定义它们才能存在。
  • 你可以提到这个细节。我认为从不可复制的类派生并使派生的类可复制是相当不寻常的。
【解决方案4】:

如果你的 operator= 变成虚拟的,我认为你会遇到问题。

我建议编写一个执行复制的函数(可能是静态的),然后让复制构造函数和 operator= 调用该函数。

【讨论】:

    【解决方案5】:

    是的,这是一种很好的做法,应该(几乎)一直这样做。除了折腾一个析构函数和默认构造函数(即使你把它设为私有)。

    在 James Coplien 1991 年的著作 Advanced C++ 中,这被描述为“正统规范形式”的一部分。在其中,他提倡使用默认构造函数、复制构造函数、赋值运算符和析构函数。

    一般情况下,您必须使用正统的规范形式,如果:

    • 您希望支持分配类的对象,或者希望将这些对象作为按值调用的参数传递给函数,
    • 对象包含指向被引用计数的对象的指针,或者类析构函数对对象的数据成员执行delete

    应该对程序中的任何重要类使用正统的规范形式,以实现类之间的一致性,并管理每个类在程序演变过程中日益增加的复杂性。 p>

    Coplien 提供了很多关于这种模式的原因,我在这里无法公正地解释它们。但是,已经触及的一个关键项目是清理被覆盖对象的能力。

    【讨论】:

    • 嗯,这很好,但它并没有真正回答任何问题,不是吗?你告诉我的只是我应该包括他们。不是如何,这才是问题所在。
    • +1 获取有用的信息,尽管马克是正确的,这根本不能回答他的问题
    • 我想我的意思是,比我更聪明的人已经考虑过这个问题,并且在 Coplien 的案例中写了一个完整的章节部分。因此,我并没有完全抄袭 Coplien,而是引用了一本值得一读的书。我将进行编辑以提供更多意见/答案。
    【解决方案6】:

    我认为你应该使用initializer list 初始化你的对象成员变量。如果你的变量是primitive-types,那么没关系。否则赋值不同于初始化。


    您可以通过将copy constructor 中的指针初始化为0 来使用一个小技巧,然后您可以在assignment operator 中安全地调用delete:

    Fixture::Fixture(const Fixture& f) : myptr(0) {
        *this = f;
    }
    Fixture& Fixture::operator=(const Fixture& f) {
        // if you have a dynamic array for example, delete other wise.
        delete[] myptr;
        myptr = new int[10];
        // initialize your array from the other object here.
        ......
        return *this;
    }
    

    【讨论】:

    • 我可以...但如果我什至不必在我知道它尚未初始化时尝试删除它会更好。
    • 删除 NULL 指针没有害处。无论如何,赋值运算符应该释放原始资源并分配新资源来复制其他对象的数据。
    • 我知道它是安全的,但它仍然是一项额外的操作;)IMO 不应该需要它。我知道这是一种微优化,但如果可以清楚地避免,我更愿意这样做。
    • 顺便说一句,我想给你一个 +1,但它不会让我。说你的帖子太旧了。试着稍微修改一下。
    • 如果 this==&f (se-fassignment),这个版本的赋值运算符可能会失败
    猜你喜欢
    • 2015-01-04
    • 2011-07-19
    • 1970-01-01
    • 1970-01-01
    • 2013-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多