Resource owning - Part 1 : Rule of three
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
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 ?
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:
Buffer b; b = b;
won’t go well otherwise.data
.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”.
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.
Build with bootstrap, powered by Jekyll and hosted on GitHub Pages.
2018 - Now Jérôme DUMESNIL