This tutorial explains how to design C++ classes for automatic allocation.
It is composed of several parts:
In the first part of this tutorial, we discussed the basic requirements for designing C++ classes with automatic allocation.
The given example was pretty simple. A class with only two integer values as members.
In such a case, the copy-constructor and the assignment operator implementations are trivial.
Things get a little more complicated when the class needs to manage resources.
Such resources can be dynamically allocated memory, file handles, database connections, etc.
This tutorial focuses on this specific case.
As an example, we'll be designing a class representing a string. Think of std::string
.
Keep in mind the following examples will obviously lack many possible optimisations.
We'll design the class as a wrapper for C-style strings (const char *
).
Here's our public interface:
#include <cstring> #include <cstdlib> #include <algorithm> class String { public: String( void ); String( const char * s ); String( const String & o ); String( String && o ); ~String( void ); String & operator =( const String & o ); const char * GetCString( void ) const { return this->_cp; } size_t GetLength( void ) const { return this->_len; } friend void swap( String & s1, String & s2 ) { using std::swap; swap( s1._cp, s2._cp ); swap( s1._len, s2._len ); } private: char * _cp; size_t _len; };
If you read part one of this tutorial, this should be quite clear.
Note that I already implemented the GetCString
and GetLength
member methods, are they are perfectly trivial, as well as the swap
function as it is covered by the previous tutorial.
Now let's take a look at the implementation for other members.
First of all, we want a parameter-less constructor, so we can declare empty string objects:
{ String s; }
Such objects should be valid: the GetCString
method should return an empty char
pointer (""
) rather than nullptr
and GetLength
should return 0
.
So we can simply rely on the second constructor, taking a char *
as argument, and pass an empty string:
String::String( void ): String( "" ) {}
The main constructor takes a const char *
as argument.
As we don't know where that argument comes from, we'll make a copy (using strdup
), so we can ensure our instance will remain valid.
We'll also check for a NULL
value, and fall back to an empty string (""
):
String::String( const char * s ) { if( s == nullptr ) { s = ""; } this->_cp = strdup( s ); this->_len = strlen( s ); }
As we're copying the string passed to our main constructor, we need to free
it in the destructor.
String::~String( void ) { free( this->_cp ); }
Now let's implement the copy constructor.
As you can guess, we need here to copy the C string from the other object.
We cannot simply assign the pointer value, as the two objects would then share the same pointer.
This would cause the destructor to crash, as we cannot free the same pointer twice.
So:
String::String( const String & o ): _len( o._len ) { this->_cp = strdup( o._cp ); }
Note that we also copy the string length. No reason to recompute it.
As stated in part one of this tutorial, our move constructor will actually move/steal resources from the object passed as argument.
So no string copying here, we'll simply assign the string pointer to the other object's one.
But we cannot stop here, as then the two objects would again share the same pointer value, causing the destructor to crash (double free).
So we need to reset the members of the object passed by reference.
Here, will simply set the string pointer to nullptr
.
The destructor will be fine, as it's OK to call free
on a NULL
pointer.
Also, we can safely assume that the object passed as an rvalue reference won't be used after it has been moved.
In other words:
String::String( String && o ): _cp( o._cp ) _len( o._len ) { o._cp = nullptr; o._len = 0; }
As you can see, we simply steal the resources from the passed object.
Then we set its value to default ones, so it can later be deallocated without any issue.
Now let's take a look at the assignment operator.
As stated in part one of this tutorial, the assignment operator needs to copy the values from the object passed as parameter. But first of all, it will also need to free any acquired resources:
String & String::operator =( const String & o ) { free( this->_cp ); this->_cp = strdup( o._cp ); this->_len = o._len; return *( this ); }
We could actually stop here, as our implementation is perfectly valid.
But maybe you noticed some redundant code with the copy constructor and the assignment operator.
Both need to copy the char
pointer from another object.
The only difference is that the assignment operator needs to free the previous resources before the copy can take place.
Not a big deal in our String
class example, but depending on the type of resources managed by the object, this can lead to many lines of duplicate code.
We can actually avoid this in a very nice and clean way.
In the actual implementation of the assignment operator, the source object is passed as a constant reference.
But unlike the copy constructor, this is not a requirement.
The object can also be passed by value, if needed.
What would happend then?
In part one of this tutorial, we saw that the compiler will use the copy constructor when passing objects by value.
This is great, as our copy constructor already does all the required stuff.
But we also need, in the assignment operator, to free the previous resources.
Well, we'll simply use here our swap
function, to exchange our existing values with the one of the other object.
Take a look at this implementation:
String & String::operator =( String o ) { swap( *( this ), o ); return *( this ); }
No more duplicate code! But what's actually happening?
First of all, the assigned object is no longer passed as a reference. It is now passed as a copy.
It means that the compiler will make a temporary copy before calling the assignment operator, using the copy constructor.
The lifetime of that temporary copy is determined by the scope of the assignment operator, meaning it will be deallocated right after the assignment operator returns.
So by simply swapping ourself with the other object, as it is a temporary copy, not only are we acquiring its resources, but we'll also have it free our own resources for us.
This optimisation is of course not required, but I highly recommend using it, as it keeps the assignment operator trivial, by simply relying on the copy constructor and the swap
function.