为什么复制赋值运算符必须返回引用/ const引用?

时间:2010-06-23 21:45:24

标签: c++ operator-overloading copy-constructor assignment-operator

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

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;
}

7 个答案:

答案 0 :(得分:63)

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

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

这些其他SO问题与您可能感兴趣的信息/意见有关(可能不完全是愚蠢)。

答案 1 :(得分:52)

有一点澄清,为什么最好通过引用返回operator=而不是按值返回---因为如果返回一个值,链a = b = c将正常工作。

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

但是,如果按operator=的值返回,则会在调用赋值运算符的每一次调用构造函数和析构函数!

所以,给定:

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!

总而言之,通过价值回归并没有获得任何好处,但却失去了很多。

注意:这并不是为了解决让赋值运算符返回左值的优点。请阅读其他帖子,了解为什么这可能更合适)

答案 2 :(得分:8)

当您重载operator=时,可以编写它以返回您想要的任何类型。如果您想要足够严重,可以重载X::operator=以返回(例如)某个完全不同的类YZ的实例。这通常是高度不可取的。

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

int x, y, z;

x = y = z = 0;

在这种情况下,您通常希望返回所分配类型的左值或右值。这只留下了是否返回对X的引用,对X的const引用或X(按值)的问题。

返回对X的const引用通常是一个糟糕的主意。特别是,允许​​const引用绑定到临时对象。临时的生命周期延长到它所绑定的引用的生命周期 - 但不会递归到任何可能分配给它的生命周期。这使得返回悬空引用变得容易 - const引用绑定到临时对象。该对象的生命周期延长到引用的生命周期(在函数结束时结束)。当函数返回时,引用和临时的生命周期已经结束,因此分配的是悬空引用。

当然,返回非const引用并不能提供完全的保护,但至少会让你更加努力。您仍然可以(例如)定义一些本地,并返回对它的引用(但大多数编译器都可以并且也会对此发出警告)。

返回值而不是引用具有理论和实际问题。在理论方面,=通常意味着在这种情况下它意味着什么。特别是,在分配通常意味着&#34;采用这个现有的源并将其值分配给这个现有的目的地&#34;,它开始意味着更像是&#34;获取这个现有的源,创建它的副本,以及将该值分配给此现有目的地。&#34;

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

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

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

答案 3 :(得分:5)

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

答案 4 :(得分:4)

可以定义

operator=以返回您想要的任何内容。你需要更具体地说明问题究竟是什么;我怀疑你的复制构造函数在内部使用operator=并导致堆栈溢出,因为复制构造函数调用operator=,它必须使用复制构造函数以无限值的方式返回A

答案 5 :(得分:3)

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

C ++98§23.1/ 3:

  

这些组件中存储的对象类型必须符合CopyConstructible的要求   类型(20.1.3),以及Assignable类型的附加要求。

C ++98§23.1/ 4:

  

在表64中,T是用于实例化容器的类型,t的值为Tu是(可能constT的值。

     

enter image description here


按值返回副本仍然会支持像a = b = c = 42;这样的赋值链,因为赋值运算符是右关联的,即它被解析为a = (b = (c = 42));。但是返回副本会禁止像(a = b) = 666;那样无意义的构造。对于一个小类来说,返回一个副本可能是最有效的,而对于一个更大的类,通过引用返回通常是最有效的(和副本,非常低效)。

在我了解标准库要求之前,我曾让operator=返回void,以提高效率并避免支持基于副作用的错误代码的荒谬。


使用C ++ 11还需要T&结果类型default - 赋值运算符,因为

C ++11§8.4.2/ 1:

  

明确默认的函数应具有相同的声明函数类型(可能不同的 ref-qualifiers 除外)   在复制构造函数或复制赋值运算符的情况下,参数类型可以是“引用非const T”,其中T是成员函数的类的名称),就好像它是隐式的一样声明

答案 6 :(得分:0)

我猜,因为用户定义的对象应该表现得像内置类型。 例如:

char c;
while ((c = getchar()) != -1 ) {/* do the stuff */}