C++ Automatic Allocation - Part 2

About

This tutorial explains how to design C++ classes for automatic allocation.

It is composed of several parts:

  1. C++ Automatic Allocation - Part 1
  2. C++ Automatic Allocation - Part 2 (this tutorial)
  3. C++ PIMPL idiom & Automatic Allocation

Table Of Contents

  1. Forewords
  2. String class interface
  3. String class implementation
    1. Parameter-less constructor
    2. Main constructor
    3. Destructor
    4. Copy constructor
    5. Move constructor
    6. Assignment operator
  4. Optimising the assignment operator

1- Forewords

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.

2 - String class interface

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.

String class implementation

3.1 - Parameter-less constructor

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( "" )
{}

3.2 - Main constructor

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 );
}

3.3 - Destructor

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 );
}

3.4 - Copy constructor

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.

3.5 - Move constructor

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.

3.6 - Assignment operator

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 );
}

4 - Optimising the assignment operator

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.

Add a comment