Saturday, December 1, 2012

Rule of Three, or how not to blow your leg off.


It's been far too long and there has been too much good stuff not to write about. This is going to be all about Move semantics. Writing safe, clean, fast code can be easy if you follow some basic principles. Memory leaks are very often minimized or eliminated if some basic principles are applied. The first is to use shared_ptr and make_shared instead of new and raw pointers. shared_ptr is such an old pattern that compiler writers and chip designers already account for it. Now that it is standard they can do even more.

Sometimes we need raw pointers. Maybe we need to make our own container or represent a bitmap. That being said, my use of raw pointers is extremely rare. However, if you do need them, or want to ignore the advice and use them anyway, you should be aware of the "rule of three."

Rule of Three

The rule of three is simple. Copy constructor, assignment operator, destructor; if you write one, you should write the other two or disable them by setting them to delete. delete and default are new C++11 features.

class Bar
{
public:
    Bar() = default;
    Bar(const Bar&) = delete;
};

Default says to use the plain old version and delete says don't make one. While it may seem silly to explicitly say I'm using the one that automatically gets created, it's important to let others know your intent.

The reason for this rule is simple. Generally we write the destructor because we have allocated a resource. Maybe it's a file or memory or whatever. If we do, we need to make sure it gets properly taken care of. If you don't delete the assignment operator, you're in for a rude surprise.

Writing an assignment operator is not an easy thing to do and is my standard whiteboard question. I have had very few candidates get it correct. Getting it right can be very tricky, unless you know the trick. The best news is that trick just got better with move semantics, but that will be another post.

So how hard could it be?

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        m_Bar = rhs.m_Bar;
        return *this;
    }
};

This is just a shallow copy, exactly what the compiler would do only wrong. We want a deep copy. We'll pretend we didn't delete the copy constructor.

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        m_Bar = new Bar(rhs.m_Bar);
        return *this;
    }
};

Well, that's a problem because we probably have a memory leak. Let's try again.

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        delete m_Bar;
        m_Bar = new Bar(rhs.m_Bar);
        return *this;
    }
};

OK, but what if rhs.m_Bar = nullptr? <sigh>

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        delete m_Bar;
        if(rhs.m_Bar)
        {
            m_Bar = new Bar(rhs.m_Bar);
        }
        return *this;
    }
};

What if I do something really stupid, but with references and aliases, you know it will happen.

    Foo f1;
    ... Do stuff
    f1 = f1;

<Are you kidding?>

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        if (this == &rhs) return *this;
        delete m_Bar;
        if(rhs.m_Bar)
        {
            m_Bar = new Bar(rhs.m_Bar);
        }
        return *this;
    }
};

OK, but <now what?!?!?!> What if new throws an exception? <@#$*&(#@>
The whole body can be written in 3 lines of code.

class Foo
{
    Bar* m_Bar;
public:
    Foo& operator=(const Foo& rhs)
    {
        Bar temp(rhs);
        std::swap(rhs, *m_Bar);
        return *this;
    }
};

That is the second important rule

Copy and Swap

Now you know it, time to forget it because C++11 has something even better!

No comments:

Post a Comment