C++ 中重载赋值运算符

发布于 2024-08-25 08:30:26 字数 324 浏览 5 评论 0原文

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


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

    return *this;
}

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


( a = b ).f();

但是为什么它应该返回引用呢?如果返回值未声明为引用(比方说按值返回),在什么情况下会出现问题?

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

As I've understand, when overloading operator=, the return value should should be a non-const reference.


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

    return *this;
}

It is non-const to allow non-const member functions to be called in cases like:


( a = b ).f();

But why should it return a reference? In what instance will it give a problem if the return value is not declared a reference, let's say return by value?

It's assumed that copy constructor is implemented correctly.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

暖风昔人 2024-09-01 08:30:27

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

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

This is Item 10 of Scott Meyers' excellent book, Effective C++. Returning a reference from operator= is only a convention, but it's a good one.

This is only a convention; code that doesn't follow it will compile. However, the convention is followed by all the built-in types as well as by all the types in the standard library. Unless you have a good reason for doing things differently, don't.

空名 2024-09-01 08:30:27

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

我不确定我是否会认可返回 void (在代码审查中我可能会说它是你不应该做的事情),但我将它作为一个选项扔在那里考虑一下您是否不想担心如何处理赋值运算符的奇怪用法。


后期编辑:

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

a = b = c;

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

(a = b) = c;

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

(a = b) = c;

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

If you're worried that returning the wrong thing might silently cause unintended side effects, you could write your operator=() to return void. I've seen a fair bit of code that does this (I assume out of laziness or just not knowing what the return type should be rather than for 'safety'), and it causes few problems. The kind of expressions that need to use the reference normally returned by operator=() are pretty rarely used, and almost always easy code an alternative for.

I'm not sure I'd endorse returning void (in a code review I'd probably call it out as something you shouldn't do), but I'm throwing it out there as an option to consider if you want to not have to worry about how oddball uses of the assignment operator might be handled.


late edit:

Also, I should have originally mentioned that you can split the difference by having your operator=() return a const& - that will still permit assignment chaining:

a = b = c;

But will disallow some of the more unusual uses:

(a = b) = c;

Note that this makes the assignment operator have semantics similar to what it has in C, where the value returned by the = operator is not an lvalue. In C++, the standard changed it so the = operator returns the type of the left operand, so it is an lvalue, but as Steve Jessop noted in a comment to another answer, while that makes it so the compiler will accept

(a = b) = c;

even for built-ins, the result is undefined behavior for built-ins since a is modified twice with no intervening sequence point. That problem is avoided for non-builtins with an operator=() because the operator=() function call is a sequence point.

违心° 2024-09-01 08:30:27

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

a = b = c = d;

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

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

总共:3 个复制向量、2 个移动操作符、1 个复制操作符

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

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

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

此外我建议您通过const引用返回值,它不会让您编写棘手且不明显的代码。有了更清晰的代码,发现错误就会容易得多:) ( a = b ).f(); 最好分成两行 a=b; af();.

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

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

Returning by reference reduces the time of performing chained operations. E. g. :

a = b = c = d;

Let's see the actions which would be called, if operator= returns by value.

  1. Copy assignment opertor= for c makes c equal to d and then creates temporary anonymous object (calls copy ctor). Let's call it tc.
  2. Then operator= for b is called. Right hand side object is tc. Move assignment operator is called. b becomes equal to tc. And then function copies b to temporary anonymous, let's call it tb.
  3. The same thing happens again, a.operator= returns temporary copy of a. After operator ; all three temporary objects are destroyed

Altogether: 3 copy ctors, 2 move operators, 1 copy operator

Let's see what will change if operator= will return value by reference:

  1. Copy assignment operator is called. c becomes equal to d, reference to lvalue object is returned
  2. The same. b becomes equal to c, reference to lvalue object is returned
  3. The same. a becomes equal to b, reference to lvalue object is returned

Altogether: only three copy operators is called and no ctors at all!

Moreover I recommend you to return value by const reference, it won't allow you to write tricky and unobvious code. With cleaner code finding bugs will be much easier :) ( a = b ).f(); is better to split to two lines a=b; a.f();.

P.S.:
Copy assignment operator : operator=(const Class& rhs).

Move assignment operator : operator=(Class&& rhs).

幽梦紫曦~ 2024-09-01 08:30:27

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

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

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

If it returned a copy, it would require you to implement the copy constructor for almost all non-trivial objects.

Also it would cause problems if you declare the copy constructor private but leave the assignment operator public... you would get a compile error if you tried to use the assignment operator outside of the class or its instances.

Not to mention the more serious problems already mentioned. You don't want it to be a copy of the object, you really do want it to refer to the same object. Changes to one should be visible to both, and that doesn't work if you return a copy.

瀞厅☆埖开 2024-09-01 08:30:26

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

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

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

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

Not returning a reference is a waste of resources and a yields a weird design. Why do you want to do a copy for all users of your operator even if almost all of them will discard that value?

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

In addition, it would be surprising to users of your class, since the built-in assignment operator doesn't copy likewise

int &a = (some_int = 0); // works
不乱于心 2024-09-01 08:30:26

重载运算符时的一个很好的一般建议是“像原始类型一样执行操作”,并且分配给原始类型的默认行为也是如此。

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

A good general advice when overloading operators is 'do as primitive types do', and the default behavior of assignment to a primitive type is that.

Not returning anything could be an option, to disable assignment inside other expressions if you feel the need, but returning a copy does not make sense at all: if the caller wants to make a copy they can make it out of the reference, if they do not need the copy there is no need to generate a temporary that is not needed.

深爱成瘾 2024-09-01 08:30:26

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

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

Cause f() can modify a. (we return a non-const reference)

If we return a value (a copy) of a, f() will modify the copy, not a

送你一个梦 2024-09-01 08:30:26

我不确定您想要多久执行一次,但是类似: (a=b)=c; 需要引用才能工作。

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

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

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

此后颁布的规则(延长用于初始化引用的临时变量的寿命)至少会减轻(并可能完全解决)这个问题,但我怀疑在编写这些规则后是否有人会重新考虑这种特殊情况。它有点像一个丑陋的设备驱动程序,其中包括解决不同版本和硬件变体中的数十个错误的拼凑——它可能会被重构和简化,但没有人很确定某些看似无害的更改何时会破坏当前的某些东西有效,如果可以的话,最终没有人愿意看它。

I'm not sure how often you'd want to do it, but something like: (a=b)=c; requires a reference to work.

Edit: Okay, there is a bit more to it than that. Much of the reasoning is semi-historical. There's more reason you don't want to return an rvalue than just avoiding an unnecessary copy into a temporary object. Using a (minor) variation on an example originally posted by Andrew Koenig, consider something like this:

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

Now, assume you're using an old version of C++ where assignment returned an rvalue. In that case, (*this=other); will yield that temporary. You're then binding a reference to the temporary, destroying the temporary, and finally returning a dangling reference to the destroyed temporary.

Rules that have been enacted since (extending the life of a temporary used to initialize a reference) would at least mitigate (and might completely cure) this problem, but I doubt anybody re-visited this particular situation after those rules had been written. It was a bit like an ugly device driver that includes kludges to work around dozens of bugs in different versions and variations of the hardware -- it could probably be refactored and simplified, but nobody's quite sure when some seemingly innocuous change will break something that currently works, and ultimately nobody wants to even look at it if they can help it.

∝单色的世界 2024-09-01 08:30:26

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

If your assignment operator does not take a const reference parameter:

A& A::operator=(A&); // unusual, but std::auto_ptr does this for example.

or if the class A has mutable members (reference count?), then it is possible that the assignment operator changes the object being assigned from as well as assigned to. Then if you had code like this:

a = b = c;

The b = c assignment would occur first, and return a copy (call it b') by value instead of returning a reference to b. When the a = b' assignment is done, the mutating assignment operator would change the b' copy instead of the real b.

Another potential problem -- returning by value instead of by reference could cause slicing if you have virtual assignment operators. I'm not saying that's a good idea, but it could be a problem.

If you intend to do something like (a = b).f() then you will want it to return by reference so that if f() mutates the object, it is not mutating a temporary.

草莓酥 2024-09-01 08:30:26

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

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

In real code (i.e. not things like (a=b)=c), returning a value is unlikely to cause any compile errors, but it is inefficient to return a copy because creating a copy can often be expensive.

You can obviously come up with situation where a reference is needed, but those rarely -- if ever -- come up in practice.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文