一个拷贝构造函数/操作符/函数是否需要明确它实现的是哪一种拷贝变体?

15

昨天我在C#中问了一个关于复制对象的问题,大多数答案都集中在深拷贝浅拷贝之间的区别上,并且应明确哪种拷贝变体给定的拷贝构造函数(或运算符或函数)实现。我觉得这很奇怪。

我写了很多C++软件,这种语言非常依赖复制,但我从来没有需要多个复制变体。我使用的唯一一种复制操作是我称之为“足够深度的复制”。它执行以下操作:

  • 如果对象对成员变量具有所有权(参见组合),则递归复制。
  • 如果对象对成员变量没有所有权(参见聚合),则只复制链接。

现在,我的问题有三个:

  • 1)对象是否需要多个复制变体?
  • 2)复制函数是否需要明确其实现的复制变体?
  • 3)顺便问一下,有没有更好的术语来描述我所说的“足够深度的复制”?我问过一个相关问题,关于“深拷贝”术语的定义
4个回答

5
"深拷贝"和"浅拷贝"的区别是一种实现细节,但是如果允许其泄漏,则通常表明一个有缺陷的抽象,该缺陷很可能会在其他方面表现出来。
如果对象Foo仅为封装所包含对象的不可变方面(包括身份以外的方面)而持有对象引用,则正确的Foo副本可以包含引用的副本或指向所封装对象的副本的引用。
如果对象Foo仅为了封装对象(包括身份以外的可变和不可变方面)而持有对象引用,并且不会将对该对象的引用暴露给任何可能使它发生变异的东西,则同样适用此情况。
如果对象Foo仅持有对象引用,以封装对象(包括身份以外的可变和不可变方面),并且该对象将被改变,则Foo的正确副本必须包含对所封装对象的副本的引用。
如果对象Foo仅为封装对象的不可变方面(包括身份)而持有对象引用,则Foo的正确副本必须包含引用的副本; 它不能包含对重复对象的引用。
如果对象Foo持有对象引用,既封装可变状态又封装对象标识,则无法单独生成正确的Foo副本。只有通过复制其所附加的整个对象集才能生成正确的Foo副本。
只有在制作正确副本的步骤中使用不完整的操作时,才有意义谈论"浅拷贝"。否则,只有一个由对象引用封装的状态类型控制的正确副本深度。

4
一个对象只需要复制它需要复制的内容。虽然这个问题被标记为语言无关,而且你提到了C++,但我更喜欢用C#术语来解释(因为这是我最熟悉的)。然而,这些概念是相似的。
值类型就像结构体。它们直接存在于对象实例中。因此,当你复制对象时,你别无选择,只能复制值类型。所以,你通常不必担心这些。
引用类型就像指针,这就是麻烦所在。根据引用类型的不同,你可能需要或不需要进行深度复制。一个通用的经验法则是,如果引用类型(作为对象的成员)取决于外部对象的状态,则应该对其进行克隆。如果不是,并且永远不会,那么就不需要。
另一种思考方式是,从外部传递给你的对象可能不应该被克隆。由你的类生成的对象应该被克隆。
好吧,我撒谎了,我将使用一些C++来最好地解释我的意思。
class MyClass {
    int foo;
    char * bar;
    char * baz;

public: MyClass(int f, char * str) {
        this->foo = f;
        bar = new char[f];
        this->baz = str;
    }
};

使用这个对象时,需要处理两个字符串缓冲区。第一个是bar,由类本身创建和管理。当你克隆对象时,应该分配一个新的缓冲区。
另一方面,baz不应该被复制。事实上,你不能这样做,因为你没有足够的信息。指针应该只是被复制。
当然,foo只是一个数字。只需复制它,没有其他需要担心的问题 :)
总之,直接回答您的问题:
1. 99% 的情况下,不需要。只有一种合理的复制方式。但这种方式会有所变化。 2. 不直接。记录它是一个好主意,但任何内部内容都应该保持内部。 3. 只需使用“深复制”。你几乎永远不应该尝试克隆你无法控制的对象或指针,所以它不适用于规则 :)

我同意大部分的答案,但是有一些特殊情况。在某些情况下,资源是在外部创建并传递给构造函数(考虑依赖注入),但实例仍然拥有该资源的所有权(在C#中语义上,在C++中物理上)。在这种情况下,克隆一个外部传递的资源可能是合适的。 - David Rodríguez - dribeas
1
总会有例外的。我猜我忘记写try{} catch{}块了 ;) - Mike Caron
这只是一个关于异常处理的玩笑 :) 另外,我错过了一个冒号,那就告我吧 >_> - Mike Caron

2

大多数C++程序员不使用“浅拷贝”和“深拷贝”这些术语,因为通常只有一种复制对象的方式。在C++中尤其如此,因为编译器在许多情况下都使用复制构造函数,而程序员可以告诉它使用哪个复制构造函数,例如:

void f( std::string s );

无法告诉编译器如何复制该字符串。


我并不完全同意。在C++中,如果使用指针(*)或引用(&)作为子变量,则非常容易实现深层或浅层复制。而在C#中,您无法在构造函数或复制运算符中明确区分这种差异。 - Dimitri C.
@Dimitri,正如我在其他地方建议的那样,您需要澄清深拷贝和浅拷贝的含义。我已经使用C++超过20年了,从未听说过这些术语与之相关,并且从未见过这些术语用于描述它。 - anon

0
有点晚的答案,但是c++11基本上已经解决了这个问题:
解决方案是,如此答案中所述 何时使用哪种指针,使用不同的指针类型来表示您拥有的(共享)所有权的种类。
由于std::unique_ptr是不可复制的,因此您将被迫复制唯一指针拥有的数据。以成员所有权的方式陈述可能总是清楚地说明在哪个成员上使用哪种副本。

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