【问题标题】:Why must the copy assignment operator return a reference/const reference?为什么复制赋值运算符必须返回引用/常量引用?
【发布时间】:2011-03-07 13:19:38
【问题描述】:

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

【问题讨论】:

  • 没有这个要求。但是如果你想坚持最小惊喜的原则,你会返回A&amp;,就像a=b是一个引用a的左值表达式,如果ab是整数。
  • @MattMcNabb 谢谢你让我知道!会这样做
  • 为什么我们不能从复制赋值运算符返回A* 我猜链接赋值仍然可以正常工作。如果有的话,任何人都可以帮助了解返回A* 的危险。
  • 注意:由于 C++11 也有 move-assignment 运算符,因此本问答中的所有相同逻辑也适用于 move-assignment 运算符。事实上,如果声明为A &amp; operator=(A a);,它们可能都是同一个函数,即按值获取参数。
  • @Krishna_Oza 真正的问题是你为什么要返回一个指针。想想如果我们只有指针,运算符重载和返回的代码会有多丑陋和模棱两可 - 在关键情况下,致命模棱两可(也:致命丑陋)。然后只需阅读语言的创造者自己的话:stackoverflow.com/questions/8007832/…

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


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    关于为什么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!
    

    总而言之,价值回归并没有得到什么,反而会失去很多。

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

    【讨论】:

    • 我觉得烦人的是你可以返回一个愚蠢的类型。我认为它应该强制执行该类型的非常量引用......虽然要删除该函数,但它返回的内容并不重要。
    【解决方案3】:

    当您重载operator= 时,您可以编写它以返回您想要的任何类型。如果你想得够严重,你可以重载X::operator= 以返回(例如)一些完全不同的类YZ 的实例。不过,这通常非常不可取。

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

    int x, y, z;
    
    x = y = z = 0;
    

    在这种情况下,您通常希望返回被分配类型的左值或右值。剩下的问题是是返回对 X 的引用、对 X 的 const 引用还是 X(按值)。

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

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

    返回值而不是引用既有理论问题,也有实际问题。从理论上讲,= 通常的含义与在这种情况下的含义之间存在基本的脱节。特别是,赋值通常意味着“获取这个现有的源并将其值分配给这个现有的目标”,它开始意味着更像“获取这个现有的源,创建它的副本,并将该值分配给这个现有的目标。 "

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

    唯一的其他选项(在 C++11 中添加)是返回一个右值引用。这很容易导致意想不到的结果——像a=b=c; 这样的链式赋值可能会破坏b 和/或c 的内容,这是非常出乎意料的。

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

    【讨论】:

    • +1 不确定悬而未决的参考讨论,但 +1 表示“你可以”。
    • 我没有看到您在“返回一个常量引用”部分中指的是哪种危险情况;如果有人写const T &amp;ref = T{} = t;,那么无论operator= 是返回T&amp; 还是T const &amp;,它都是一个悬空引用。讽刺的是,如果 operator= 按值返回就好了!
    • @MattMcNabb:哎呀——应该说是lvalue reference。感谢您指出这一点(因为是的,右值引用显然在这里是个坏主意)。
    【解决方案4】:

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

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

    这些其他 SO 问题是相关的(可能不完全是骗人的),其中包含您可能感兴趣的信息/意见。

    【讨论】:

    • 我发现当我需要防止对象从堆栈中自动销毁时,在赋值运算符上返回 void 很有用。对于引用计数的对象,您不希望在您不了解它们时调用析构函数。
    【解决方案5】:

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

    C++98 §23.1/3:

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

    C++98 §23.1/4:

    在表 64 中,T 是用于实例化容器的类型,tT 的值,u 是(可能是 @987654329 @)T


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

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


    对于 C++11,default-ing 赋值运算符还需要 T&amp; 结果类型,因为

    C++11 §8.4.2/1:

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

      • 那将是一个蹩脚的(和不寻常的)copy-ctor 实现。在大多数情况下,从A::operator= 返回A&amp; 的原因是不同的。
      • @jpalecek,我同意,但鉴于原始帖子和在说明实际问题时缺乏清晰度,由于无限递归,赋值运算符执行很可能导致堆栈溢出。如果这个问题有其他解释,我很想知道。
      • @MSN 我不知道是不是他的问题。但你在这里的帖子肯定已经解决了我的问题 +1
      【解决方案7】:

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

      【讨论】:

      • 不会投反对票,但我想指出按值返回是没有意义的。想象一下 (a = b = c) 如果 (a = b) 按值返回“a”。你的后一点非常合理。
      • 我相信你会得到 (a = (b = c)),它仍然会产生预期的结果。只有当你做了 (a = b) = c 才会被破坏。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-02-22
      • 1970-01-01
      • 2013-05-26
      • 2011-02-25
      • 2018-01-26
      • 2015-05-31
      相关资源
      最近更新 更多