为什么拷贝分配运算符必须返回引用/常量引用?


71

在C ++中,从复制赋值运算符返回引用的概念对我来说还不清楚。为什么复制分配运算符不能返回新对象的副本?另外,如果我有classA和以下内容:

A a1(param);
A a2 = a1;
A a3;

a3 = a2; //<--- this is the problematic line

operator=定义如下:

A A::operator=(const A& a)
{
    if (this == &a)
    {
        return *this;
    }
    param = a.param;
    return *this;
}

3
没有这样的要求。但是,如果你要坚持至少surprize你会回来的原理A&就像a=b是一个左值表达式引用a的情况下,ab是整数。
sellibitze 2010年

@MattMcNabb谢谢您让我知道!会做到这一点
bks 2015年

为什么我们不能A*从副本分配运算符返回,我猜链接分配仍然可以正常工作。任何人都可以帮助您了解返回的危险A*
克里希纳·奥扎2015年

1
注意:由于C ++ 11中也有移动分配运算符,因此本问答中的所有相同逻辑也适用于移动分配运算符。实际上,如果将它们声明为A & operator=(A a);,则它们可能都是同一函数,即按值接受参数。
MM

@Krishna_Oza真正的问题是为什么要返回一个指针。想想如果我们只有指针,操作符重载和返回的代码将是多么丑陋和模棱两可-在关键情况下,是致命的歧义(也:致命的丑陋)。然后,只需阅读该语言的创建者自己的话即可:stackoverflow.com/questions/8007832/…–
underscore_d

Answers:


71

严格来说,拷贝赋值运算符的结果不需要返回引用,尽管为了模仿C ++编译器使用的默认行为,它应该返回对分配给该对象的非常量引用(隐式生成的拷贝赋值运算符将返回非常量引用-C ++ 03:12.8 / 10)。我已经看到了很多void从副本分配重载返回的代码,并且我不记得何时引起了严重的问题。退回void将阻止用户进行“分配链”(a = b = c;),并会阻止在测试表达式中使用赋值结果。尽管这种代码绝不是闻所未闻的,但我也不认为它特别常见-特别是对于非原始类型(除非类的接口打算用于此类测试,例如iostream)。

我不建议您这样做,只是指出它是允许的,并且似乎不会引起很多问题。

这些其他SO问题与您可能感兴趣的信息/观点相关(可能不是很重复)。


我发现在需要防止对象从堆栈中自动销毁时,在赋值运算符上返回void很有用。对于引用计数对象,您不希望在不知道析构函数的情况下调用它们。
cjcurrie 2013年

60

关于为什么最好通过引用返回而operator=不是按值返回的原因,澄清了一点,因为如果返回值,该链a = b = c将可以正常工作。

如果您返回参考,则只需进行最少的工作。来自一个对象的值将复制到另一对象。

但是,如果按的值返回operator=,则将调用构造函数AND析构函数每次调用赋值运算符的时间!

因此,鉴于:

A& operator=(const A& rhs) { /* ... */ };

然后,

a = b = c; // calls assignment operator above twice. Nice and simple.

但,

A operator=(const A& rhs) { /* ... */ };

a = b = c; // calls assignment operator twice, calls copy constructor twice, calls destructor type to delete the temporary values! Very wasteful and nothing gained!

总而言之,按价值回报并没有获得任何收益,但却有很多损失。

注意:这并不是要解决让赋值运算符返回左值的优点。请阅读其他文章,以了解为什么这样更好)


10

当您重载时operator=,您可以编写它以返回所需的任何类型。如果您想做的足够糟糕,则可以重载X::operator=以返回(例如)某些完全不同的类Y或的实例Z。但是,这通常是非常不可取的。

特别是,您通常希望operator=像C一样支持链接。例如:

int x, y, z;

x = y = z = 0;

在这种情况下,通常需要返回分配给该类型的左值或右值。剩下的问题是是否返回对X的引用,对X的const引用或X(按值)。

将const引用返回给X通常不是一个好主意。特别是,允许​​const引用绑定到临时对象。临时对象的生存期延长到其绑定的引用的生存期,但不递归地扩展到可能分配给它的任何对象的生存期。这使得返回悬挂的引用变得很容易-const引用绑定到临时对象。该对象的生存期延长到引用的生存期(在函数末尾终止)。在函数返回时,引用和临时对象的生存期已经结束,因此分配的是悬空的引用。

当然,返回非const引用并不能完全避免这种情况,但是至少会使您更加努力。您仍然可以(例如)定义一些局部变量,并返回对它的引用(但是大多数编译器也可以并且也会对此发出警告)。

返回值而不是引用具有理论和实践问题。从理论上讲,=在这种情况下,通常含义与含义之间存在基本的分离。特别是,在分配通常意味着“获取此现有资源并将其值分配给该现有目的地”的地方,它开始意味着更像“获取该现有资源,创建它的副本,并将该值分配给该现有目的地”。 ”

从实践的角度来看,尤其是在发明右值引用之前,这可能会对性能产生重大影响-在将A复制到B的过程中创建一个完整的新对象是意料之外的,而且通常很慢。例如,如果我有一个较小的向量,并将其分配给一个较大的向量,则我希望最多花费时间来复制该较小向量的元素,再加上(少量)固定开销来调整该向量的大小。目标向量。如果相反地涉及两份副本,一份从源到临时,另一份从临时到目的地,并且(更糟)为临时向量动态分配,我对操作复杂性的期望是完全是毁了。对于较小的向量,动态分配的时间可能很容易比复制元素的时间高很多倍。

唯一的其他选择(在C ++ 11中添加)是返回右值引用。这很容易导致意外的结果-像这样的链式分配a=b=c;可能会破坏band / or的内容c,这将是非常意外的。

这使得返回普通引用(不是对const的引用,也不是右值引用)作为唯一的选项(合理地)可靠地产生大多数人通常想要的东西。


+1不确定悬而未决的参考讨论,但+1表示“可以”。
干杯和健康。-Alf 2014年

在“返回const引用”部分中,我看不到您指的是哪种危险情况。如果有人写作const T &ref = T{} = t;,不管operator=返回T&还是返回,都是悬空的参考T const &。具有讽刺意味的是,operator=按值返回就可以了!
MM

@MattMcNabb:糟糕-应该这么说lvalue reference。感谢您指出这一点(因为是的,这里的右值引用显然不是一个好主意)。
杰里·科芬,2015年

5

部分原因是返回对self的引用要比按值返回更快,但除此之外,它还允许原始类型中存在原始语义。


不会拒绝投票,但我想指出,按价值回报毫无意义。想象一下(a = b = c)如果(a = b)按值返回'a'。您的后一点很合法。
stinky472 2010年

4
我相信您会得到(a =(b = c)),但仍会产生预期的结果。只有做到了(a = b)= c,它才会被破坏。
小狗

4

operator=可以定义为返回您想要的任何东西。您需要更具体地了解问题的实质。我怀疑您在operator=内部使用了复制构造函数,这会导致堆栈溢出,因为复制构造函数调用operator=必须使用复制构造函数A按值无限制地返回。


那将是一个la脚的(也是不寻常的)copy-ctor实现。理由退换货A&A::operator=在大多数情况下的不同。
jpalecek 2010年

@jpalecek,我同意,但是鉴于原始帖子并且在陈述实际问题时不够清楚,由于无限递归,执行赋值运算符很可能导致stackoverflow。如果对此问题还有其他解释,我想知道。
MSN 2010年

@MSN我不知道这是不是他的问题。但可以肯定的是,您在此处的帖子已解决了我的问题+1
Invictus

3

用户定义的结果类型没有核心语言要求 operator=,但是标准库确实有这样的要求:

C ++ 98§23.1/ 3:

类型存储在这些部件的对象必须满足的要求CopyConstructible 类型(20.1.3),和的附加要求Assignable的类型。

C ++ 98§23.1/ 4:

在表64,T是用于实例化容器的类型,t是的值T,和u是的值(可能constT

在此处输入图片说明


按值返回副本仍将支持赋值链接,例如a = b = c = 42;,因为赋值运算符是右关联的,即被解析为a = (b = (c = 42));。但是归还副本将禁止类似的无意义的构造(a = b) = 666;。对于一个小类,返回一个副本可能是最有效的,而对于一个大类,通过引用返回通常是最有效的(而一个副本,效率低下)。

在我了解标准库要求之前,我曾经让operator=returnvoid来提高效率,并避免了支持基于副作用的不良代码的荒谬性。


在C ++ 11中,另外需要对赋值运算符进行赋值的T&结果类型default,因为

C ++ 11§8.4.2/ 1:

明确默认为默认值的函数应[...]具有相同的声明函数类型(除了可能不同的ref限定符,以及对于复制构造函数或复制赋值运算符而言,参数类型可以是“对非const T”,其中T成员函数的类的名称),就好像它已经隐式声明一样

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.