据我所理解,当重载 operator= 时,返回值应该是一个非const引用。
A& A::operator=( const A& )
{
// check for self-assignment, do assignment
return *this;
}
为了允许在如下情况下调用非常量成员函数,它是非常量的:
( a = b ).f();
但是为什么它应该返回一个引用?如果返回值不声明为引用,比如按值返回,会在什么情况下出现问题?
假定复制构造函数已正确实现。
据我所理解,当重载 operator= 时,返回值应该是一个非const引用。
A& A::operator=( const A& )
{
// check for self-assignment, do assignment
return *this;
}
为了允许在如下情况下调用非常量成员函数,它是非常量的:
( a = b ).f();
但是为什么它应该返回一个引用?如果返回值不声明为引用,比如按值返回,会在什么情况下出现问题?
假定复制构造函数已正确实现。
不返回引用会浪费资源并导致奇怪的设计。为什么要为运算符的所有用户创建副本,即使几乎所有用户都会放弃该值?
a = b; // huh, why does this create an unnecessary copy?
另外,如果您的类使用内置赋值运算符时也不会进行类似的复制,这将令用户感到惊讶。
int &a = (some_int = 0); // works
在重载运算符时一个好的建议是“像基本类型一样进行”,而对于基本类型的赋值,其默认行为就是这样。
如果你觉得有必要,在其他表达式中禁用赋值可以选择不返回任何东西,但是返回副本根本没有意义:如果调用者想要拷贝,他们可以从引用中进行复制,如果他们不需要拷贝,那么生成一个不需要的临时变量是没有必要的。
因为f()可以修改a。(我们返回一个非const引用)
如果我们返回a的值(即复制品),f()将修改副本,而不是a。
我不确定您希望多频繁地执行此操作,但是像这样的代码:(a=b)=c;
需要使用引用才能正常工作。
编辑:好吧,实际上还有更多内容。很多原因都半历史性质的。避免将值返回作为右值的原因不仅仅是为了避免将其复制到临时对象中。以 Andrew Koenig 最初发布的示例为基础进行(轻微)变化,考虑以下代码:
struct Foo {
Foo const &assign(Foo const &other) {
return (*this = other);
}
};
(* this = other); 将产生该临时变量。然后,您绑定到该临时变量的引用,销毁该临时变量,并最终返回对已销毁临时变量的悬挂引用。
自那之后实施的规则(扩展用于初始化引用的临时对象的生命周期)至少会减轻(并可能完全解决)此问题,但我怀疑任何人在这些规则编写后重新访问了这种特殊情况。这有点像一个丑陋的设备驱动程序,其中包括解决硬件不同版本和变体中数十个错误的修补程序-它可能可以重构和简化,但如果他们能帮助的话,最终没有人确定某些看似无害的更改会破坏当前工作的东西,最终没有人想要甚至看它。
(a=b)=c
这样的代码会受到惩罚,因为对于内置类型来说这是未定义行为。但如果operator=
是函数调用,则不会受到惩罚。 - Steve Jessop如果您的赋值运算符没有使用const引用参数:
A& A::operator=(A&); // unusual, but std::auto_ptr does this for example.
如果类 A
有可变成员(引用计数?),那么赋值运算符可能会改变被赋值的对象以及被赋值的对象。因此,如果你有以下代码:
a = b = c;
b = c
赋值将首先发生,并通过值返回一个副本(称之为 b'
),而不是返回对 b
的引用。当完成 a = b'
赋值时,变异赋值运算符将更改 b'
副本而不是真正的 b
。(a = b).f()
的操作,则需要按引用返回,以便如果 f()
改变对象,则不会改变临时对象。通过引用返回可以减少执行链接操作的时间。例如:
a = b = c = d;
operator=
返回值的情况下会调用哪些操作。
c
的复制赋值运算符使c
等于d
,然后创建一个临时匿名对象(调用复制构造函数)。让我们称之为tc
。b
的operator=。右侧对象是tc。调用了移动赋值运算符。 b
变成等于tc
。然后函数将b
复制到临时的匿名对象,让我们称之为tb
。a.operator=
返回a
的临时副本。在运算符;
之后,所有三个临时对象都被销毁c
变为等于 d
,返回左值对象的引用。b
变为等于 c
,返回左值对象的引用。a
变为等于 b
,返回左值对象的引用。总之:仅调用了三个复制运算符,没有调用任何构造函数!
此外,我建议您通过 const 引用返回值,这样可以防止编写棘手和不明显的代码。使用更干净的代码将更容易找到错误 :) ( a = b ).f();
最好拆分成两行 a=b; a.f();
。
P.S.:
复制赋值运算符: operator=(const Class& rhs)
。
移动赋值运算符: operator=(Class&& rhs)
。
如果你担心返回错误的内容可能会悄悄地导致意外的副作用,你可以编写你的operator=()
返回void
。我见过很多这样的代码(我认为是因为懒惰或者不知道应该返回什么类型而不是出于“安全”考虑),并且它几乎没有问题。通常需要使用operator=()
返回的引用的表达式非常少见,而且几乎总是可以轻松编写替代方案。
我不确定我是否支持返回void
(在代码审查中,我可能会指出它是不应该做的事情),但我提出这个选项是为了考虑一下,如果你想不必担心赋值运算符的奇怪用法如何处理。
最后编辑:
另外,我一开始应该提到你可以通过让你的operator=()
返回一个const&
来折中解决 - 这仍然允许赋值链:
a = b = c;
但会禁止一些更不寻常的用法:
(a = b) = c;
=
运算符返回的值不是左值。在C++中,标准将其更改为=
运算符返回左操作数的类型,因此它是左值,但正如Steve Jessop在对另一个答案的评论中指出的那样,虽然这使得编译器接受了这种写法,但它并没有真正地将右侧的值分配给左侧的变量。(a = b) = c;
a
,结果是未定义的行为。对于非内置函数,通过使用 operator=()
避免了这个问题,因为 operator=()
函数调用是一个序列点。(a=b)=c
这样的情况),返回一个值不太可能导致任何编译错误,但是返回副本是低效的,因为创建副本通常很昂贵。这是Scott Meyers的优秀书籍《Effective C++》中的第10项。从operator=
返回引用只是一种约定,但它是一个好习惯。
这只是一种约定;不遵循它的代码也会编译。然而,所有内置类型以及标准库中的所有类型都遵循这个约定。除非你有充分的理由采取不同的做法,否则不要这样做。
如果它返回一个副本,那么几乎所有非平凡对象都需要实现复制构造函数。
此外,如果您将复制构造函数声明为私有但将赋值运算符声明为公共,则会出现问题...如果您尝试在类或其实例之外使用赋值运算符,则会得到编译错误。
更不用说已经提到的更严重的问题了。你不想它是对象的副本,你真的想让它引用同一个对象。对一个对象的更改应该对两个对象都可见,如果返回一个副本,这是行不通的。
void
。这将防止(a=b)=c
、a=(b=c)
或任何其他可能显示值和引用之间差异的恶作剧发生。 - Steve Jessop