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

Can we say bye to copy constructors?

本文关键字:构造函数 复制 说再见 我们      更新时间:2023-10-16

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

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

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

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

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

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

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

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


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

  • 可复制资源的 RAII 句柄(很明显(
  • 旨在像内置类型一样使用的结构,如数学向量或矩阵 -
    仅仅因为它们经常被复制并且vec3 b = a.copy()太冗长了。

旁注:我已经考虑过 CAS 需要复制构造函数的事实,但 CAS 需要 operator=(const T&) 基于完全相同的推理,我认为这是多余的;
如果您真的需要这个,最好使用 .copy() + operator=(T&&) = default

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

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

具体来说,我是否正确,移动构造函数在 C++11 中完全接管了对象重新分配的责任?我非正式地使用"重新分配"来表示需要将对象移动到内存中的其他地方而不更改其状态的所有情况。

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

如果对象是变量引用的资源(如在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+的资源,因此将其移动到a,而不是将其复制到另一个a保留的位置并删除它更有效。

移动和复制是两回事。移动不是"复制的替代品"。这是一种更有效的方法,仅在不需要对象生成自身克隆的所有情况下避免复制。

short anwer

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

自动生成的复制

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

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

长答案

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

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

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

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

这与RAII类型几乎没有区别;它们只需要声明一个不同名称的复制函数,用户只需要稍微更详细一点。

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

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

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

并解决您提出的赞成意见:

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

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

为了说明这一点,让我们考虑一下std::vectorresize函数。该函数可以大致实现如下:

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 函数具有强大的异常保证,我们需要确保在引发异常时,保留resize()调用之前的std::vector状态。

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

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

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

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

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

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

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

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