C++中的重载赋值运算符

Posted

技术标签:

【中文标题】C++中的重载赋值运算符【英文标题】:Overloading assignment operator in C++ 【发布时间】:2011-01-27 17:00:57 【问题描述】:

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


A& A::operator=( const A& )

    // check for self-assignment, do assignment

    return *this;

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


( a = b ).f();

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

假设拷贝构造函数实现正确。

【问题讨论】:

如果您希望人们将赋值更像是语句而不是表达式,您可以返回void。这将阻止(a=b)=ca=(b=c) 和任何其他可能揭示值和引用之间差异的恶作剧。 我发现当我需要防止对象从堆栈中自动销毁时,在赋值运算符上返回 void 很有用。对于引用计数的对象,您不希望在您不了解它们时调用析构函数。 【参考方案1】:

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

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

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

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

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

【讨论】:

是的,确实需要。但你是对的,这样的代码很奇怪。 有很多“C+”程序员做着可怕的“聪明”事情。我经常看到这样的恐怖事件,以至于我觉得自己生活在一部低成本的砍杀电影中。 @Tadeusz:写(a=b)=c 会受到惩罚,因为它是内置类型的未定义行为。只是在 operator= 是函数调用时不会。 圣经第 10 项,我的意思是有效的 c++,说从 operator=() 返回 *this 的原因是允许链接分配。 @Graphics Noob:是的,我读过。但它并没有说这就是原因。即使实现为按值返回,你仍然可以说 a = b = c;它仍然有效。【参考方案2】:

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

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

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

int &a = (some_int = 0); // works

【讨论】:

是的,我知道这是一种浪费。我只是想知道是否存在按值或引用返回使赋值操作错误/不正确值的情况。 @Johannes:对不起,我没听懂你的最后一句话。需要解释一下吗? @jasonline:obj1=obj2 返回一个临时值。当有人使用您的类尝试创建对 (obj1=obj2) 的引用时,会看到:1- 如果它是非常量引用,它将无法编译,2- 它会创建对临时对象的引用(而不是 obj1或 obj2) 这会使他们感到困惑,因为原始类型不能那样工作(参见 litb 的示例)。 @tiftik:您是说像 A& z = (x = y) 这样的东西不会编译,因为 (x = y) 返回的内容是临时的,而您的引用不是 const? @jasonline:是的。你试过编译吗?【参考方案3】:

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

不返回任何内容可能是一种选择,如果您认为需要禁用其他表达式中的赋值,但返回副本根本没有意义:如果调用者想要制作副本,他们可以将其从引用中取出,如果他们不需要副本,则无需生成不需要的临时副本。

【讨论】:

【参考方案4】:

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

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

【讨论】:

为什么不让它返回一个常量引用?这样你就不会复制,也不能修改返回的对象。【参考方案5】:

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

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

【讨论】:

【参考方案6】:

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

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

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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


后期编辑:

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

a = b = c;

但会禁止一些更不寻常的用途:

(a = b) = c;

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

(a = b) = c;

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

【讨论】:

@Michael:感谢您对 C 和 C++ 中的差异以及顺序点的额外(和清晰)解释。我从没想过有什么不同。无论如何,我只关心如何以正确的方式实现它(比如原语如何实现)以及为什么要以这种方式实现它。我无意让它返回 void,因为它会禁用链接,这通常是允许的。【参考方案9】:

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

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

【讨论】:

是的,我已经读过这个。我实际上是在寻找一些会导致错误值的实例,但我想大多数答案都是效率问题。【参考方案10】:

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

a = b = c = d;

让我们看看如果operator= 按值返回,会调用哪些操作。

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

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

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

    调用了复制赋值运算符。 c 等于 d,返回对左值对象的引用 一样。 b 等于 c,返回对左值对象的引用 一样。 a 等于 b,返回对左值对象的引用

总共:只调用了三个复制操作符,根本没有操作符!

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

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

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

【讨论】:

以上是关于C++中的重载赋值运算符的主要内容,如果未能解决你的问题,请参考以下文章

C++ 继承多态关系中的赋值运算符的重载=operator()

如何为从C++中的模板继承的类重载赋值运算符

C++中赋值运算操作符和=重载有啥区别?

C++重载赋值运算符

C++重载赋值运算符

c++中为啥赋值运算符重载返回类型是引用