为什么C++的拷贝构造函数必须使用const对象?

29

我理解当我们定义一个类时,根据C++编程中的三法则(Rule of three),类的复制构造函数是必需的。我还注意到复制构造函数的参数通常为const,如下面的代码所示:

class ABC {
public:
    int a;
    int b;
    ABC(const ABC &other)
    { 
        a = other.a;
        b = other.b;
    }
}

我的问题是,如果拷贝构造函数的参数不是const会发生什么:

class ABC
{
    public:
    int a;
    int b;
    ABC(ABC &other)
    { 
        a = other.a;
        b = other.b;
    }
}

我知道,如果复制构造函数的参数是const,那么第二个实现可能会失败。此外,如果复制构造函数的参数是const,那么在复制过程中被复制的对象不会改变其内容。但是,我注意到有些人仍然使用第二种实现而不是第一种。有没有任何原因使第二种实现更受欢迎?


4
为什么应该在ABC B(A)中修改A?这样做意义不大且行为相当不直观。 - juanchopanza
10
可能是因为作者忘记将其声明为const。 - Oliver Charlesworth
3
也可能有些人试图像 "auto_ptr" 一样使用巧计。当然,鉴于即使是标准委员会也无法做到正确地使用它,这是一个非常糟糕的想法。 - celtschk
1
我可以想象复制构造函数需要使用&other的方法,这些方法本身没有声明为“const”。希望这样做的方式不会改变&other - flaschenpost
1
@flaschenpost:那些方法应该声明为const - celtschk
显示剩余5条评论
8个回答

41
  • 从逻辑上讲,修改一个你想要复制的对象没有意义,但有时可能会有一些意义,比如你想存储此对象已被复制的次数。但是这可以使用mutable成员变量来实现,它可以存储此信息,并且即使是const对象也可以进行修改(第二个点将证明这种方法的正确性)。

  • 您希望能够创建const对象的副本。但如果您没有使用const限定符传递参数,则无法创建const对象的副本...

  • 您无法从临时引用创建副本,因为临时对象是右值,无法绑定到非const引用。更详细的解释,请参阅Herb Sutter关于此问题的文章


7
这里还有一个事实,接受非常量引用版本的函数将无法用于临时对象。 - juanchopanza
仅供参考:即使是引用计数智能指针也不需要可变性。引用计数器并不位于智能指针本身,而通常位于特殊的中间对象或引用对象内部(侵入式实现)。因此,原始的_smartpointer_ 保持不变,并且可以声明为const。 - user396672
@user396672,我会进行修正以删除误导性的示例。 - JBL

14

任何使用您的类的消费者都不会期望一个改变被复制对象的拷贝构造函数存在!因此,您应该始终将其标记为const。


12

这里需要使用const的两个原因:

  1. 它确保在制作副本时不会意外“损坏”原始对象 - 这是一件好事,因为你真的不想在制作副本时改变原始对象!
  2. 你可以传入除基本对象之外的其他内容 - 因为构造函数采用引用,如果它本身不是一个对象 - 比如说一个表达式。

为了举例第二种情况:

 class ABC
    {
       public:
           int a;
           int b;
       ABC(const ABC &other)
       { 
         a = other.a;
         b = other.b;
       }
       ABC operator+(const ABC &other)
       {
           ABC res;
           res.a = a + other.a;
           res.b = b + other.b;
           return res;
       }
    }

  ...
  ABC A;
  a.a = 1;
  a.b = 2;
  ABC B(a+a);

如果构造函数是 ABC(ABC &other),那么这段代码将无法编译,因为a+a 是一个类型为 ABC 的临时对象。但如果构造函数是 ABC(const ABC &other),我们可以使用计算的临时结果,并将其作为引用传递。


6

正如其他答案所指出的那样,修改其参数的复制构造函数将是一个令人不愉快的惊喜。然而,这不是唯一的问题。复制构造函数有时会与临时参数一起使用。 (例如:从函数返回。)非const引用不能使用临时参数,如此处在SO上解释。


3

如果复制构造函数没有将其参数指定为const,则此片段将无法编译。

const ABC foo;
ABC bar(foo);

3
也许更加狡猾的是,这样写也可以:ABC bar(some_func_returning_ABC()); - Angew is no longer proud of SO

1

复制构造函数不应该修改正在复制的对象,这就是为什么在other参数上使用const更受欢迎的原因。两者都可以工作,但是const更受欢迎,因为它清楚地说明传入的对象不应该被函数修改。

const仅供用户使用,它并不存在于实际可执行文件中。


2
不完全正确;如果对象执行引用计数,则rhs可能需要被修改。 - Oliver Charlesworth
4
我认为这应该是一个“mutable”成员,因为通常情况下引用计数不是可观察状态的一部分。 - Andy Prowl
@AndyProwl:同意。我只是想驳斥rhs从不被修改的说法 ;) - Oliver Charlesworth
1
如果对象正在使用引用计数,则引用计数不能是源对象的一部分(即直接或间接子对象),因此rhs仍不应被修改(尽管它“部分拥有”或指向的东西可以被修改),并且仍应为const。 - CB Bailey
@AndyProwl:绝对不行!如果引用计数是可变的,它仍然是源对象的子对象,并且在源对象的生命周期结束时将被销毁。这对于副本来说是不好的,因为现在(可能)有一个指向不再存在的引用计数的指针。 - CB Bailey
显示剩余3条评论

1

0

在技术意义上并非“必须”的。我们甚至在标准中拥有这样的怪兽,尽管它已被弃用。请点击链接查看原因。

我们期望的复制语义是将“模板”保持不变,并提供一个完全等效的克隆体,您很难从原始对象区分。

既然如此,你应该三思而后行,不要使用其他方式的复制构造函数。它会给用户带来惊喜,并可能引入错误。还有挫败感和噪音,只需尝试搜索“vector of auto_ptr”即可看到计数。

问题的其余部分可能是“我发誓不会在实现中触及原始对象,但想要不同的签名”。那么什么签名?让我们尝试 T 和 T&。

T 被排除在外,因为它需要可用的复制构造函数,而我们正在实现它。递归:见递归

剩下的就是 T&。这在很多情况下确实有效。但如果您的原始对象恰好以 const 形式存在或者是临时对象,它将会失败。为什么要为了毫无意义的事情阻碍明智的情况呢?


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