3

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

即使程序逻辑不需要复制对象,复制构造函数(通常是默认的)也经常被包含在对象重新分配的唯一目的中。如果没有复制构造函数,您将无法将对象存储在 a 中std::vector,甚至无法从函数中返回对象。

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

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

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

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

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

  4. 最后但同样重要的.copy()是,如果需要,方法可以是虚拟的,从而可以解决切片问题。


我真正想要使用复制构造函数的唯一情况是:

  • 可复制资源的 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

4 回答 4

5

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

如果对象是变量引用的资源(如在 java 或 C++ 中通过指针,使用经典的 OOP 范例)每个“变量之间的复制”都是“共享”,如果强加单一所有权,“共享”变成“移动” .

如果对象本身就是变量,因为每个变量都必须有自己的历史,如果你不能/不想强行破坏一个值以支持另一个值,你就不能“移动”。

例如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"被丢弃。

本质上, move 仅在显式调用或正确的参数是“临时”时才有效,例如

  a = b+c;

operator+在分配后显然不需要通过 return 保留的资源,因此将其移动到a,而不是将其复制到另一个a保留的位置并删除它更有效。

移动和复制是两个不同的东西。移动不是“副本的替代品”。它是一种更有效的方法,可以避免仅在不需要对象生成自身克隆的所有情况下进行复制。

于 2013-05-08T19:20:43.163 回答
3

简答

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

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

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

长答案

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

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

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

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

这对 RAII 类型几乎没有影响;他们只需要声明一个不同名称的复制函数,用户只需要稍微详细一点。

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

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

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

并解决您赞成的观点:

  1. 复制(非多态)对象司空见惯的,尽管正如您所说,现在可以在可能的情况下移动它们已经不太常见了。这只是您认为“明确更好”或T a(b);不那么明确的意见T a(b.copy());
  2. 同意,如果一个对象没有明确定义的复制语义,那么它应该有命名函数来涵盖它提供的任何选项。我看不出这会如何影响正常对象的复制方式。
  3. 我不知道为什么你认为不应该允许复制构造函数做命名函数可以做的事情,只要它们是定义的复制语义的一部分。您认为不应使用复制构造函数,因为您自己对它们施加了人为限制。
  4. 复制多态对象是完全不同的鱼。仅仅因为多态函数必须强制所有类型使用命名函数不会给出您似乎在争论的一致性,因为返回类型必须不同。多态副本需要通过指针动态分配和返回;非多态副本应按值返回。在我看来,使这些不同的操作看起来相似但不能互换几乎没有价值。
于 2013-05-09T10:54:13.467 回答
1

事情就是这样。搬家是新的默认设置——新的最低要求。但是复制通常仍然是一种有用且方便的操作。

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

我不会很快放弃复制构造函数,但我承认,对于我自己的类型,我只在明确需要它们时才添加它们——而不是立即添加。到目前为止,这是非常非常少的类型。

于 2013-05-09T09:03:38.900 回答
1

复制构造函数派上用场的一种情况是在实现强异常保证时。

为了说明这一点,让我们考虑 的resize函数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没有移动构造函数,那么我们将默认使用复制构造函数。在这种情况下,如果复制构造函数抛出异常,我们仍然可以提供强大的异常保证:我们只是简单地deletenewData数组并没有造成任何伤害std::vector

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

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

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

如果 n 小于或等于容器的大小,则该函数从不抛出异常(不抛出保证)。如果 n 更大并且发生重新分配,则如果元素的类型是可复制的或不可抛出可移动的,则容器中不会发生异常(强保证)的更改。否则,如果抛出异常,容器将保持有效状态(基本保证)。

于 2013-05-08T19:42:39.947 回答