Resource owning - Part 1 : Rule of three

Published on Jul 20, 2018 | Last edited on Jul 28, 2018 |
C++
Rule of three
Single Responsibility Principle

I’ve seen too much code base mixing technical code with business one. Particulary, a lot of business code is owning technical resource. Trying to figure out what this code does is often a lot of pain. The resource owning code deserve its own class, independent of the business code. This is a good programming practice, well described in the Single Responsibility Principle. In this post, we will see what are the good rules of thumb to design a resource owning class, starting with the Rule of three.

This post is the first part of a series about Resource owning:

Let’s start with a good use case : The circular buffer

Use case: The circular buffer

Let’s say we have to write an application that process data packet coming from network, a serial line or whatever input interface. After few minutes of reflexion, we come up with a design consisting in two parts. The first one is responsible for fetching a chunk of data from the physical interface and for forwarding it to the second part, which will do all the business processing related to this data packet. To effectively manage these data packets, we opted for a big circular buffer, allocated just once, which can store several packets. So, in accordance to the single responsibility principle, the circular buffer ressource deserve its own class. Let’s name it Buffer for code brevity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Buffer
{
public:
    explicit Buffer(size_t capacity);
    ~Buffer(void);

    bool isEmpty(void) { ... }
    bool isFull(void) { ... }

    void write(const uint8_t * data, size_t size) { ... }
    void read (      uint8_t * data, size_t size) { ... }

private:
    uint8_t * data;
    size_t    capacity;
    int       start;
    int       end;
};

This is a simple basic implementation of a circular buffer. The data points to a heap allocated buffer of capacity bytes. start and end are indexes used to delimit the occupied part of the buffer. The listing above shows the implementation of the constructor and desctructor.

1
2
3
4
5
6
7
8
9
10
11
explicit Buffer::Buffer(size_t _capacity)
: data(_capacity ? new uint8_t[_capacity] : nullptr)
, capacity(_capacity)
, start(-1)
, end(-1)
{ }

Buffer::~Buffer(void)
{
    delete [] data;
}

The buffer capacity is passed as argument to the constructor and used to dynamically allocate the underlying data buffer. Other members are initialized so that the buffer is in a valid empty state. Obviously, we free the data buffer in the destructor.

But what if we would copy this buffer ? What about the copy construction and the copy assignment ?

The rule of three

In our case, the copy constructor and copy assignment operator automaticaly generated by the compiler won’t do the job. They will just copy the pointer value of data. And when the first Buffer will be destroyed, the second one will point to a freed memory, which is a Really Bad Thing . So when talking about resource owning, the first good rule of thumb is given by the Rule of three :

If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.

So here we go for the copy constructor:

1
2
3
4
5
6
7
8
Buffer::Buffer(const Buffer & buffer)
: data(buffer.capacity ? new uint8_t[buffer.capacity] : nullptr)
, capacity(buffer.capacity)
, start(buffer.start)
, end(buffer.end)
{
    std::copy_n(buffer.data, capacity, data);
}

The data of the newly created buffer is allocated with the same capacity as the copied buffer. Then, the buffer content is copied using std::copy_n() from the standard library. Other members are also copied.

Now for the copy assignment operator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Buffer & Buffer::operator =(const Buffer & buffer)
{
    // Prevent self assignment
    if(&buffer != this)
        return *this;

    // Cleanup old data
    delete [] data;

    // Allocate new one
    data     = buffer.capacity ? new uint8_t[buffer.capacity] : nullptr;
    capacity = buffer.capacity;
    start    = buffer.start;
    end      = buffer.end;
    std::copy_n(buffer.data, capacity, data);

    return *this;
}

The code is pretty straight forward:

  • First (lines 4 to 5), we need to prevent from self assignment… Code like Buffer b; b = b; won’t go well otherwise.
  • Next (line 8), we cleanup the current buffer by freeing data.
  • Finally (lines 11 to 15), we copy the passed buffer, like we did in the copy constructor.

Obviously, don’t forget to return a reference to the current buffer in order to allow usage like:

1
2
Buffer a, b, c;
a = b = c;

This implementation raises some remarks. First, self assignment is very very rare, so the test on line 4 will be false most of the time. Executing useless code 99.99% of the time isn’t very pleasant. Secondly, the rest of the code is a duplication of destructor and copy constructor, which violates the Don’t Repeat Yourself principle. And last, but not least, what about exception safety ? What will happen if we get out of memory and the new operator raises an std::bad_alloc ? data will be a dangling pointer, our Buffer will go into an invalid state and accessing it may result in surprising behavior.

But don’t worry, we will fix that in the next part of the series: “Resource owning - Part 2 : Rule of five”.

Conclusion

Today, we have seen that when talking about resource owning there are some good pratices to use. Single Responsibility Principle should be applied to separate the resource handling code from the business one. Also, if one of destructor, copy constructor or copy assignment operator is defined, all of them must also be defined, as stated by the Rule of three.

Share this post!

Build with bootstrap, powered by Jekyll and hosted on GitHub Pages.

2018 - Now Jérôme DUMESNIL