91
votes

Since a copy constructor

MyClass(const MyClass&);

and an = operator overload

MyClass& operator = (const MyClass&);

have pretty much the same code, the same parameter, and only differ on the return, is it possible to have a common function for them both to use?

3
"...have pretty much the same code..."? Hmm... You must be doing something wrong. Try to minimize the need to use user-defined functions for this and let the compiler do all the dirty work. This often means encapsulating resources in their own member object. You could show us some code. Maybe we have some good design suggestions.sellibitze

3 Answers

127
votes

Yes. There are two common options. One - which is generally discouraged - is to call the operator= from the copy constructor explicitly:

MyClass(const MyClass& other)
{
    operator=(other);
}

However, providing a good operator= is a challenge when it comes to dealing with the old state and issues arising from self assignment. Also, all members and bases get default initialized first even if they are to be assigned to from other. This may not even be valid for all members and bases and even where it is valid it is semantically redundant and may be practically expensive.

An increasingly popular solution is to implement operator= using the copy constructor and a swap method.

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

or even:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

A swap function is typically simple to write as it just swaps the ownership of the internals and doesn't have to clean up existing state or allocate new resources.

Advantages of the copy and swap idiom is that it is automatically self-assignment safe and - providing that the swap operation is no-throw - is also strongly exception safe.

To be strongly exception safe, a 'hand' written assignment operator typically has to allocate a copy of the new resources before de-allocating the assignee's old resources so that if an exception occurs allocating the new resources, the old state can still be returned to. All this comes for free with copy-and-swap but is typically more complex, and hence error prone, to do from scratch.

The one thing to be careful of is to make sure that the swap method is a true swap, and not the default std::swap which uses the copy constructor and assignment operator itself.

Typically a memberwise swap is used. std::swap works and is 'no-throw' guaranteed with all basic types and pointer types. Most smart pointers can also be swapped with a no-throw guarantee.

14
votes

The copy constructor performs first-time initialization of objects that used to be raw memory. The assignment operator, OTOH, overrides existing values with new ones. More often than never, this involves dismissing old resources (for example, memory) and allocating new ones.

If there's a similarity between the two, it's that the assignment operator performs destruction and copy-construction. Some developers used to actually implement assignment by in-place destruction followed by placement copy-construction. However, this is a very bad idea. (What if this is the assignment operator of a base class that called during assignment of a derived class?)

What's usually considered the canonical idiom nowadays is using swap as Charles suggested:

MyClass& operator=(MyClass other)
{
    swap(other);
    return *this;
}

This uses copy-construction (note that other is copied) and destruction (it's destructed at the end of the function) -- and it uses them in the right order, too: construction (might fail) before destruction (must not fail).

-3
votes

Something bothers me about:

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    swap(tmp);
    return *this;
}

First, reading the word "swap" when my mind is thinking "copy" irritates my common sense. Also, I question the goal of this fancy trick. Yes, any exceptions in constructing the new (copied) resources should happen before the swap, which seems like a safe way to make sure all the new data is filled before making it go live.

That's fine. So, what about exceptions that happen after the swap? (when the old resources are destructed when the temporary object goes out of scope) From the perspective of the user of the assignment, the operation has failed, except it didn't. It has a huge side effect: the copy did actually happen. It was only some resource cleanup that failed. The state of the destination object has been altered even though the operation seems from the outside to have failed.

So, I propose instead of "swap" to do a more natural "transfer":

MyClass& operator=(const MyClass& other)
{
    MyClass tmp(other);
    transfer(tmp);
    return *this;
}

There's still the construction of the temporary object, but the next immediate action is to free all current resources of the destination before moving (and NULLing so they won't be double-freed) the resources of the source to it.

Instead of { construct, move, destruct }, I propose { construct, destruct, move }. The move, which is the most dangerous action, is the one taken last after everything else has been settled.

Yes, destruction fail is a problem in either scheme. The data is either corrupted (copied when you didn't think it was) or lost (freed when you didn't think it was). Lost is better than corrupted. No data is better than bad data.

Transfer instead of swap. That's my suggestion anyway.