18

据我了解,重载 operator= 时,返回值应该是非常量引用。


A& A::operator=( const A& )
{
    // check for self-assignment, do assignment

    return *this;
}

在以下情况下允许调用非常量成员函数是非常量的:


( a = b ).f();

但是它为什么要返回一个引用呢?如果没有将返回值声明为引用,那么在什么情况下会出现问题,比如说按值返回?

假定复制构造函数已正确实现。

4

10 回答 10

18

不返回参考是一种资源浪费,并且会产生一个奇怪的设计。即使几乎所有用户都会丢弃该值,为什么还要为您的运营商的所有用户制作副本?

a = b; // huh, why does this create an unnecessary copy?

此外,您的班级的用户会感到惊讶,因为内置的赋值运算符不会同样复制

int &a = (some_int = 0); // works
于 2010-03-15T14:11:23.500 回答
16

重载运算符时的一个很好的一般建议是“像原始类型一样做”,而分配给原始类型的默认行为就是这样。

不返回任何东西可能是一种选择,如果您觉得需要禁用其他表达式中的赋值,但返回一个副本根本没有意义:如果调用者想要制作一个副本,他们可以将其从引用中取出,如果他们不需要副本没有必要生成不需要的临时文件。

于 2010-03-15T14:14:03.727 回答
4

因为f()可以修改一个. (我们返回一个非常量引用)

如果我们返回a的值(副本),f()将修改副本,而不是a

于 2010-03-15T14:15:57.360 回答
3

我不确定您希望多久执行一次,但类似:(a=b)=c;需要对工作的引用。

编辑:好的,还有更多。大部分推理都是半历史性的。您不想返回右值的原因不仅仅是避免将不必要的副本复制到临时对象中。使用最初由 Andrew Koenig 发布的示例的(次要)变体,考虑如下内容:

struct Foo { 
    Foo const &assign(Foo const &other) { 
        return (*this = other);
    }
};

现在,假设您使用的是旧版本的 C++,其中赋值返回了一个右值。在那种情况下,(*this=other);将产生那个临时的。然后绑定对临时对象的引用,销毁临时对象,最后返回对被销毁临时对象的悬空引用。

此后制定的规则(延长用于初始化引用的临时对象的寿命)至少可以缓解(并且可能完全治愈)这个问题,但我怀疑在编写这些规则之后是否有人重新访问过这种特殊情况。它有点像一个丑陋的设备驱动程序,其中包含解决不同版本和硬件变体中的数十个错误的组件——它可能会被重构和简化,但没有人很确定什么时候一些看似无害的更改会破坏当前的某些东西工作,最终没有人愿意看它,如果他们可以帮助它。

于 2010-03-15T14:11:12.243 回答
2

如果您的赋值运算符不采用 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()改变对象,它就不会改变临时对象。

于 2010-03-15T14:48:58.407 回答
1

在实际代码中(即不是类似的东西(a=b)=c),返回值不太可能导致任何编译错误,但返回副本效率低下,因为创建副本通常会很昂贵。

您显然可以提出需要参考的情况,但在实践中很少(如果有的话)出现。

于 2010-03-15T14:23:24.163 回答
1

如果你担心返回错误的东西可能会默默地导致意想不到的副作用,你可以写你的operator=() to return void。我已经看到了很多这样做的代码(我假设是出于懒惰或只是不知道返回类型应该是什么,而不是为了“安全”),并且它引起的问题很少。需要使用通常返回的引用的那种表达式operator=()很少使用,而且几乎总是简单的代码替代。

我不确定我是否会支持返回void(在代码审查中,我可能会将其称为你不应该做的事情),但我将它作为一个选项扔掉,以考虑你是否不想拥有担心如何处理赋值运算符的古怪用法。


后期编辑:

另外,我最初应该提到,您可以通过operator=()返回 a const&- 来分割差异,这仍然允许分配链接:

a = b = c;

但将不允许一些更不寻常的用途:

(a = b) = c;

请注意,这使得赋值运算符具有类似于它在 C 中的语义,其中=运算符返回的值不是左值。在 C++ 中,标准对其进行了更改,因此=运算符返回左操作数的类型,因此它是一个左值,但正如 Steve Jessop 在对另一个答案的评论中指出的那样,编译器将接受

(a = b) = c;

即使对于内置插件,结果也是内置插件的未定义行为,因为a它被修改了两次,没有中间的序列点。对于带有 an 的非内置函数,可以避免该问题,operator=()因为operator=()函数调用是一个序列点。

于 2010-03-15T15:12:00.357 回答
1

这是 Scott Meyers 的优秀著作Effective C++的第 10 项。从那里返回引用operator=只是一种约定,但它是一个很好的约定。

这只是一个约定;不遵循它的代码将编译。但是,所有内置类型以及标准库中的所有类型都遵循该约定。除非你有充分的理由以不同的方式做事,否则不要这样做。

于 2010-03-15T15:18:12.190 回答
1

通过引用返回减少了执行链式操作的时间。例如。:

a = b = c = d;

operator=如果按值返回,让我们看看将被调用的操作。

  1. 复制赋值 opertor= forc使c等于d然后创建临时匿名对象(调用复制 ctor)。让我们称之为tc
  2. 然后 operator= forb被调用。右手边的对象是 tc。调用移动赋值运算符。b变得等于tc。然后将函数复制b到临时匿名,我们称之为tb.
  3. 同样的事情再次发生,a.operator=返回a. 在操作员之后;所有三个临时对象都被销毁

总共:3 个复制操作员,2 个移动操作员,1 个复制操作员

让我们看看如果 operator= 将通过引用返回值会发生什么变化:

  1. 调用复制赋值运算符。c等于d,返回对左值对象的引用
  2. 相同。b等于c,返回对左值对象的引用
  3. 相同。a等于b,返回对左值对象的引用

总共:只调用了三个复制运算符,根本没有 ctors!

此外,我建议您通过 const 引用返回值,它不会让您编写棘手和不明显的代码。使用更简洁的代码查找错误会容易得多:)( a = b ).f();最好分成两行a=b; a.f();

PS:复制赋值运算符:operator=(const Class& rhs)

移动赋值运算符 : operator=(Class&& rhs).

于 2016-04-27T20:53:31.527 回答
0

如果它返回一个副本,则需要您为几乎所有非平凡对象实现复制构造函数。

此外,如果您将复制构造函数声明为私有但将赋值运算符设为公开,则会导致问题......如果您尝试在类或其实例之外使用赋值运算符,则会出现编译错误。

更不用说已经提到的更严重的问题了。您不希望它成为对象的副本,您确实希望它引用同一个对象。对一个的更改应该对双方都可见,如果您返回副本,这将不起作用。

于 2010-03-15T14:23:57.093 回答