Overloading assignment operator in C++
Asked Answered
A

10

20

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.

Abe answered 15/3, 2010 at 14:8 Comment(2)
If you want people to treat assignment a bit more like a statement rather than an expression, you could maybe return void. That will stop (a=b)=c, a=(b=c) and any other such shenanigans that might have revealed the difference between a value and a reference.Patman
I found it useful to return void on the assignment operator when I needed to prevent the automatic destruction of objects as they came off the stack. For ref-counted objects, you don't want destructors being called when you don't know about them.Henka
F
20

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
Fong answered 15/3, 2010 at 14:11 Comment(7)
Yes, I understand it's a waste. I'm just wondering if there's case where returning by value or reference makes the assignment operation wrong/incorrect value.Abe
@Johannes: Sorry, I don't get your last sentence. Care to explain?Abe
@jasonline: obj1=obj2 returns a temporary value. when someone using your class tries to create a reference to (obj1=obj2) will see that: 1- it won't compile if it's a non-const reference, 2- it will create a reference to a temporary object (not to obj1 or obj2) which will confuse them since primitive types don't work that way (see litb's example).Tody
@tiftik: Are you saying that something like A& z = (x = y) won't compile, since what is returned by (x = y) is a temporary and your reference is not const?Abe
@jasonline: Yes. Did you try to compile it?Tody
@tiftik: Yes, I understand your point. I'm just wondering why it compiles sucessfully in VC++ 2008, declaring operator= to return by value.Abe
@jasonline, VC++ uses a non-standard extension. Put warning levels up and you will see it warn. The temporary won't bind to a non-const reference in strict standard mode.Fong
C
16

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.

Cornflower answered 15/3, 2010 at 14:14 Comment(0)
S
4

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

Sissie answered 15/3, 2010 at 14:15 Comment(1)
Why not make it return a const reference? This way you won't make a copy and you can't modify the returned object either.Brisco
S
3

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.

Scarcity answered 15/3, 2010 at 14:11 Comment(6)
Yes, it does need. But you're right, it's weird to code like that.Abe
There are a lot of "C+" programmers out there who do horribly "clever" things like that. I see horrors like this so often that I feel like I'm living in a low-budget slasher flick.Christlike
@Tadeusz: Writing (a=b)=c sort of is punished, in that it's undefined behaviour for built-in types. Just not when operator= is a function call.Patman
Item 10 of the bible, I mean effective c++, says the reason to return *this from operator=() is to allow chaining of assignments.Unabridged
@Graphics Noob: Yes, I've read that. But it didn't exactly say that's the reason. Even if implemented as return by value, you can still say a = b = c; and it still worked.Abe
@GraphicsNoob that's a misreading of what is written there. The author states it is because that's the behavior of built-in types.Narda
C
2

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.

Charbonnier answered 15/3, 2010 at 14:48 Comment(0)
A
1

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.

Achaemenid answered 15/3, 2010 at 14:23 Comment(0)
H
1

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.

Hockenberry answered 15/3, 2010 at 15:12 Comment(1)
@Michael: Thanks for additional (and clear) explanation about that difference in C and C++, and the sequence points. I never thought there was a difference. Anyway, I'm just concerned how to implement it the right way (like how primitives do) and why implement it that way. I don't have any intention of having it to return void since it would disable chaining, which should be normally allowed.Abe
H
1

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.

Haggle answered 15/3, 2010 at 15:18 Comment(1)
Yes, I've read about this. I was actually looking for some instance where it would cause an incorrect value, but I guess most of the answers are efficiency issues.Abe
T
1

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).

Tonguelash answered 27/4, 2016 at 20:53 Comment(0)
C
0

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.

Civilize answered 15/3, 2010 at 14:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.