The rule of three

Published Nov 20, 2017

Back in the day C++ 98 Compiler generated following for you

  1. Default Constructor
  2. Copy Constructor
  3. Copy Assignment
  4. Destructor

Consider following Class Foo

class foo 
{
  public:
  foo(std::size_t i = 10)
    :buffer(new uint8_t [i])
    ,head(buffer)
    ,tail(buffer)
  {}
  ~foo()
  {
    delete []buffer;
  }
  private:
  uint8_t *buffer, *head, *tail;
};

So what’s the problem with this code?

int main (int argc, const char ** argv)
{
  foo f1;
  foo f2 = f1;
  return 0;
}

You get double free Exception because you are deleting same buffer twice.
Let’s talk about some fixes in this code

  1. Delete destructor
    The code runs fine after deleting destructor but causes meory leaks.
  2. Remove Copy constructor by adding
foo(const foo &) = delete;
void operator = (const foo &) = delete; 

But let’s say we still want to be able to copy it, so we need to overload copy constructor and ‘=’ operator

class foo 
{
  public:
  foo(std::size_t i = 10)
    :buffer(new uint8_t [i])
    ,head(buffer)
    ,tail(buffer)
    ,size(i)
  {}
  ~foo()
  {
    delete []buffer;
  }
  foo (const foo & rhs)
    :buffer{new uint8_t [rhs.size]}
    ,head{0}
    ,tail{0}
    ,size(rhs.size)
  {
    std::copy(rhs.head, rhs.tail, buffer);
    head = buffer + (rhs.head - rhs.buffer);
    tail = buffer + (rhs.tail - rhs.buffer);
  }
  foo & operator = (const foo & rhs)
  {
    if(&rhs != this)
    {
      if(rhs.size > size)
      {
        delete[] buffer;
        buffer = new uint8_t[size];
      }
      size = rhs.size;
      std::copy(rhs.head, rhs.tail, buffer);
      head = buffer + (rhs.head - rhs.buffer);
      tail = buffer + (rhs.tail - rhs.buffer);
    }
    return *this; 
  }
  private:
  uint8_t *buffer, *head, *tail;
  std::size_t size;
};

Now, we have deduced that having impemented destructor requires both copy constructor and assignment operator to be implemented.
But is the opposite true?
Do we need to implement our own destructor if we have implemented the other two?
Let’s implement the foo class again

class foo 
{
  public:
  foo()
    :head(buffer)
    tail(buffer)
  {}
  private:
  uint8_t *buffer, *head, *tail;
  std::size_t size;
  foo (const foo & rhs)
  {
    std::copy(rhs.head, rhs.tail, buffer);
    head = buffer + (rhs.head - rhs.buffer);
    tail = buffer + (rhs.tail - rhs.buffer);
  }
  foo & operator = (const foo & rhs)
  {
    if(&rhs != this)
    {
      std::copy(rhs.head, rhs.tail, buffer);
      head = buffer + (rhs.head - rhs.buffer);
      tail = buffer + (rhs.tail - rhs.buffer);
    }
    return *this; 
  }
  private:
  uint8_t buffer[10], *head, *tail;
};

So, you don’t need a destructor whenever you see a copy constructor and/or an assignment operator.
The Rule of Three is really two rules:

  • If a class has a nonempty destructor, it almost always needs a copy constructor and an assignment operator.
  • If a class has a nontrivial copy constructor or assignment operator, it usually needs both of these members and a destructor as well.
Discover and read more posts from Dhruv Sehgal
get started