运算符 = 的值参数会导致奇怪的编译错误

Value parameters to operator= cause strange compilation error

本文关键字:编译 错误 值参 参数 运算符      更新时间:2023-10-16

我试图重新学习C++,我倾向于对一切是如何工作的有一个复杂的理解(而不仅仅是"我该怎么做")。 所以我想知道为什么这会产生它所做的错误。 是的,我知道重载赋值运算符应该使用引用(如果我这样做,它可以正常工作),但我希望这个问题的答案可以帮助我更多地了解语言规则。

class some_class {
public:
    int n1;
    some_class(int z) : n1(z) { }
    some_class(some_class &x) : n1(x.n1) { }
    some_class operator= (some_class x) { n1 = x.n1; return *this; }
//  some_class & operator= (some_class & x) { n1 = x.n1; return *this; } works fine
};
main () {
    some_class a(10);
    some_class b(20);
    some_class c(30);
    c = b = a;          // error here
}

编译器(C++03)在c = b = a行中给了我这个:

In function 'int main()':
   error: no matching function for call to 'some_class::some_class(some_class)'
   note: candidates are: some_class::some_class(some_class&)
   note:                 some_class::some_class(int)
   error:   initializing argument 1 of 'some_class some_class::operator=(some_class)'

这让我感到困惑,因为b = a工作正常,而且它正在寻找一个法律不允许我声明的构造函数。 我意识到在c = b = a中,b = a部分返回一个值(而不是引用),这可能会导致结果被复制到临时。 但是,为什么c = <temporary>会导致编译错误,而b = a却不会呢? 知道发生了什么吗?

复制构造函数有一个非常量引用作为其参数。临时不能绑定到非常量引用。当您执行以下操作时:

c = b = a;

这等同于(如您所说):

c.operator=(<temporary>);

因此,它尝试在初始化调用 operator= 的第一个参数时临时调用您的复制构造函数。由于上述原因,此操作失败。修复它的一个明智方法是将operator=的签名更改为更传统的签名:

some_class& operator=(const some_class& x);

在实现operator=时不需要复制构造函数,因为operator=的参数不会被复制。但是,复制构造函数通常应采用 const 引用参数,因此还应将复制构造函数的签名更改为:

some_class(const some_class& x);

导致错误的是你的复制构造函数,它应该是

some_class(const some_class&)

这是因为您无法将临时对象传递给非常量引用,而链接分配中会发生这种情况。这是因为赋值运算符按值返回,这将创建一个临时对象,然后将其作为值参数传递给下一个赋值运算符。这将调用复制构造函数,该构造函数具有非 const 引用参数,因此无法绑定到临时对象。

赋值运算符应为

some_class& operator= (some_class x)

some_class& operator= (const some_class& x)

也就是说,它采用相同类型的值或常量引用参数,并返回一个非常量引用,即*this 。或者,它可以具有 void 返回类型以防止链接。

我知道其他变体是"允许的",但除非您知道自己在做什么,否则不要使用它们。

除非您有 value 参数的原因(例如复制和交换习惯用法),否则您应该使用 const 引用来防止额外的复制。

some_class(some_class &x) : n1(x.n1) { }
some_class operator= (some_class x) { n1 = x.n1; return *this; }

应该是

some_class(const some_class &x) : n1(x.n1) { }
some_class& operator=(const some_class& x) { n1 = x.n1; return *this; }

使用原始签名时,无法复制或分配示例中所示的 const 临时对象。

如果我们不使用引用,关键问题还在于不必要的数据副本。

还有另一种方法可以解决这个问题,鉴于您正在尝试重新学习 c++,了解它会很有趣。C++11 引入了 r 值引用和 std::move,以允许在复制构造函数和赋值运算符中使用临时变量以减少内存副本。

移动复制构造函数和赋值运算符如下所示

some_class(some_class &&x) : n1(std::move(x.n1)) {}
some_class& operator=(some_class&& x) { n1 = std::move(x.n1); return *this; }

在语句中:

c = b = a;

由于赋值运算符具有从右到左的关联性,因此首先执行b = a。由于赋值运算符返回值,因此调用复制构造函数。由于临时对象不能由非常量引用引用(正如上面已经提到的 Stuart Golodetz ),编译器会寻找 some_class::some_class(some_class x) 类型的构造函数;并抱怨找不到它。

仅当将复制构造函数的签名修改为:

some_class(const some_class& x);

正如其他人提到的,赋值运算符传统上返回一个引用,因为我们可以链接其中的几个。但是,并非必须如此设计它们。