我们可以说复制构造函数吗?

时间:2013-05-08 18:55:13

标签: c++ c++11 clone copy-constructor

复制构造函数传统上在C ++程序中无处不在。但是,我怀疑自从C ++ 11以来是否有充分的理由。

即使程序逻辑不需要复制对象,复制构造函数(usu.default)通常仅用于对象重新分配。如果没有复制构造函数,则无法在std::vector中存储对象,甚至无法从函数返回对象。

但是,自从C ++ 11以来,移动构造函数一直负责对象的重新分配。

复制构造函数的另一个用例是简单地制作对象的克隆。但是,我确信.copy().clone()方法比复制构造函数更适合该角色,因为......

  1. 复制对象并不常见。当然,对象的界面有时需要包含“自制副本”方法,但有时只是。在这种情况下,显式优于隐式。

  2. 有时,对象可能会暴露几种不同的.copy()类方法,因为在不同的上下文中,副本可能需要以不同的方式创建(例如,更浅或更深)。

  3. 在某些情况下,我们希望.copy()方法执行与程序逻辑相关的非平凡事情(增加一些计数器,或者可能为副本生成新的唯一名称)。我不接受任何在复制构造函数中具有非明显逻辑的代码。

  4. 最后但并非最不重要的是,如果需要,.copy()方法可以是虚拟的,可以解决slicing的问题。


  5. 我实际上想要使用复制构造函数的唯一情况是:

    • RAII处理可复制资源(很明显)
    • 拟用于内置类型的结构,如数学向量或矩阵 -
      只是因为它们经常被复制而且vec3 b = a.copy()过于冗长。
      

    旁注:我已经考虑过CAS需要复制构造函数,但operator=(const T&)需要CAS,我认为这是基于完全相同的推理的冗余;
      如果您真的需要,.copy() + operator=(T&&) = default将是首选。)

    对我来说,这足以激励默认情况下在任何地方使用T(const T&) = delete并在需要时提供.copy()方法。 (也许还有一个private T(const T&) = default只是为了能够在没有样板的情况下编写copy()virtual copy()。)

    问:上述推理是正确的还是我错过了逻辑对象实际需要或以某种方式从复制构造函数中获益的任何正当理由?

    具体来说,我是否正确,移动构造函数完全接管了C ++ 11中对象重新分配的责任?当一个对象需要在内存中的其他位置移动而不改变其状态时,我正在非正式地使用“重新分配”。

4 个答案:

答案 0 :(得分:4)

问题在于“对象”这个词是指什么。

如果 objects 是变量所引用的资源(比如java或C ++中的指针,使用经典的OOP范例),每个“变量之间的副本”都是“共享”,如果是单一所有权强制执行,“分享”变得“移动”。

如果对象是变量本身,由于每个变量都必须有自己的历史记录,如果你不能/不想强制破坏某个值,就不能“移动”另一个。

Cosider例如std::strings

   std::string a="Aa";
   std::string b=a;
   ...
   b = "Bb";

您是否期望a的值发生变化,或者该代码无法编译?如果没有,则需要复制。

现在考虑一下:

   std::string a="Aa";
   std::string b=std::move(a);
   ...
   b = "Bb";

现在a为空,因为它的值(更好,包含它的动态内存)已“移动”到b。然后对b的值进行检查,丢弃旧的"Aa"

本质上,只有在显式调用或右边的参数是“临时”时,移动才有效,如

  a = b+c;

在分配后显然不需要返回operator+所持有的资源,因此将其移至a,而不是将其复制到另一个a所持有的位置,删除它更有效。

移动和复制是两回事。移动不是“复制的替代品”。这是一种更有效的方法,只有在对象不是 required 生成自身克隆的所有情况下才能避免复制。

答案 1 :(得分:2)

Short anwer

  

上述推理是正确的还是我错过了逻辑对象实际需要或以某种方式从复制构造函数中获益的任何正当理由?

自动生成的拷贝构造函数在将资源管理与程序逻辑分离方面是一个很大的好处;实现逻辑的类根本不需要担心分配,释放或复制资源。

在我看来,任何替换都需要做同样的事情,对命名函数这样做感觉有点奇怪。

答案很长

在考虑复制语义时,将类型分为四类是很有用的:

  • 原语类型,由语言定义的语义;
  • 具有特殊要求的资源管理(或RAII)类型;
  • 聚合类型,只复制每个成员;
  • 多态类型。

原始类型就是它们,因此它们超出了问题的范围;我假设对语言的彻底改变,打破了几十年的遗留代码,不会发生。在没有用户定义的虚函数或RTTI恶作剧的情况下,无法复制多态类型(保持动态类型),因此它们也超出了问题的范围。

所以提议是:要求RAII和聚合类型实现命名函数,而不是复制构造函数,如果它们应该被复制。

这对RAII类型没什么影响;他们只需要声明一个不同名称的复制函数,用户只需要稍微冗长一点。

但是,在当前世界中,聚合类型根本不需要声明显式复制构造函数;将自动生成一个以复制所有成员,或者如果任何成员不可复制则删除。这确保了,只要所有成员类型都可以正确复制,聚合也是如此。

在你的世界里,有两种可能性:

  • 语言知道您的复制功能,并且可以自动生成一个(可能仅在明确请求时,即T copy() = default;,因为您需要显式)。在我看来,基于相同命名函数在其他类型中自动生成命名函数感觉更像是当前生成“语言元素”(构造函数和运算符重载)的方法,但也许这只是我的偏见。
  • 或者由用户正确实现聚合的复制语义。这很容易出错(因为你可以添加一个成员并忘记更新函数),并打破资源管理和程序逻辑之间当前的清晰分离。

并提出你赞成的观点:

  1. 复制(非多态)对象 是常见的,尽管正如你所说的那样现在它们不太常见,因为它们可以在可能的情况下被移动。只是您认为“明确更好”或T a(b);不如T a(b.copy());
  2. 更明确
  3. 同意,如果一个对象没有明确定义的复制语义,那么它应该有命名函数来覆盖它提供的任何选项。我不知道这会如何影响正常对象的复制方式。
  4. 我不知道为什么你认为复制构造函数不应该被允许做命名函数可以做的事情,只要它们是定义的复制语义的一部分。你认为不应该使用复制构造函数,因为你自己会对它们进行人为限制。
  5. 复制多态物体是一个完全不同的鱼群。强制所有类型使用命名函数只是因为多态的函数必须不会给出你似乎在争论的一致性,因为返回类型必须是不同的。多态拷贝需要动态分配并由指针返回;应按值返回非多态副本。在我看来,使这些不同的操作看起来相似而没有可互换性几乎没有价值。

答案 2 :(得分:1)

复制构造函数有用的一种情况是实现强异常保证

为了说明这一点,我们考虑resize的{​​{1}}函数。该功能可大致如下实现:

std::vector

如果void std::vector::resize(std::size_t n) { if (n > capacity()) { T *newData = new T [n]; for (std::size_t i = 0; i < capacity(); i++) newData[i] = std::move(m_data[i]); delete[] m_data; m_data = newData; } else { /* ... */ } } 函数具有强大的异常保证,我们需要确保,如果抛出异常,则保留resize调用之前std::vector的状态。

如果resize()没有移动构造函数,那么我们将默认使用复制构造函数。在这种情况下,如果复制构造函数抛出异常,我们仍然可以提供强大的异常保证:我们只需T delete数组,并且不会对newData造成任何损害。

但是,如果我们使用std::vector的移动构造函数并抛出异常,那么我们会将一堆T移到T数组中。回滚这个操作并不简单:如果我们尝试将它们移回newData数组,m_data的移动构造函数可能会再次抛出异常!

要解决此问题,我们有T功能。如果std::move_if_noexcept标记为T,则此函数将使用noexcept的移动构造函数,否则将使用复制构造函数。这使我们能够以提供强大异常保证的方式实施std::vector::resize

为了完整起见,我应该提到C ++ 11 std::vector::resize在所有情况下都不提供强大的异常保证。根据www.cplusplus.com,我们有以下保证:

  

如果n小于或等于容器的大小,则该函数永远不会抛出异常(无抛出保证)。   如果n更大并且发生重新分配,则在异常情况下(如果元素的类型是可复制的或无法可移动的),容器中没有变化。   否则,如果抛出异常,容器将保留有效状态(基本保证)。

答案 3 :(得分:1)

这就是事情。移动是新的默认值 - 新的最低要求。但复制仍然是一种有用且方便的操作。

没有人应该向后弯腰以提供复制构造函数。但是,如果您可以简单地提供可复制性,那么对您的用户来说仍然有用。

我不会很快抛弃复制构造函数,但我承认,对于我自己的类型,我只在它变得清晰时才需要它们 - 而不是立即。到目前为止,这种类型非常非常少。