mr-edd.co.uk :: horsing around with the C++ programming language

A problem with the rule of three

[15th March 2009]

When flicking through forums where C++ advice is doled out, the Rule of Three is often mentioned.

It can be summarized as follows:

If your class requires a user-defined destructor, copy constructor or copy assignment operator, then it probably requires all three.

It's certainly a useful guideline to internalize and it's a good thought experiment to understand where that advice comes from.

However, it's important to realize that the the negation is often not true. That is to say, if your class does not require a user-defined destructor or a copy constructor, it doesn't necessarily follow that you can forgo implementing a custom copy assignment operator.

Let's have a look at an example.

class contact
{
    public:
        contact(const std::string &name, const std::vector<std::string> &address);
        // ...

    private:
        std::string name_;
        std::vector<std::string> address_;
};

Here we have a basic contact class that might be used in a simple address book application. The only resource that is used by a contact is memory, allocated for the strings and the vector member variables. The compiler-generated destructor takes care of the deallocation for us, so we don't need to supply our own. Similarly, the compiler-generated copy constructor will do the right thing. But what about the copy assignment operator?

Under ideal circumstances, the compiler-generated assignment operator will work just fine, too. But sometimes it won't.

Such an operator will first invoke std::string's operator= to copy the name_ member and then the operator= for std::vector<std::string> will be called to copy the address_ member. So where's the catch?

What if the the vector's assignment operator throws an exception? The target object will end up with an appropriate name_, but the address_ will be incorrect. So the compiler generated copy assignment operator isn't strongly exception safe.

Now, memory exhaustion errors are few and far between these days, but this is merely a simple example. There are plenty of other reasons a copy-assignment of an arbitrary object might throw. If you have any class that contains multiple member objects whose copy assignment operator can throw, then you need to think about this issue.

The fix

So how should the copy assignment operator be written? We use the classic copy-and-swap idiom, but we have to copy all the member objects first, before doing any of the swapping.

contact &operator= (const contact &rhs)
{
    // Do all the stuff that might throw first
    std::string temp_name_(rhs.name_);
    std::vector<std::string> temp_address_(rhs.address_);

    // Commit the changes in such a way that makes an exception impossible
    name_.swap(temp_name);
    address_.swap(temp_address);

    return *this;
}

If the copy of the vector throws here, it won't matter as this object hasn't been modified at all yet.

Alternatively, you might implement contact::swap(contact &other) and then write contact's operator= in terms of its copy constructor and this new swap() function. The basic idea is still the same.

Comments

tulcod

[09/04/2009 at 12:32:00]

I'm sorry, how exactly does this solve the exception safety guarantee? If address_.swap(temp_address); throws, then name_ has already been modified, am I right?

tulcod

[09/04/2009 at 15:42:00]

I'm sorry, STL containers provide nothrow swap. :)

Nicolas

[28/06/2009 at 19:17:00]

I think std::swap, for ANY object, is NOT ALLOWED to throw.

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.