Sunday, 8 January 2012

Default constructors (3)

Last time, I mentioned two options that were available for maintaining your non-default-constructible class without needing to weaken its invariant.

Let's say you have the following class, as provided by Stroustrup:

class File_handle {
public:
    File_handle(const char* n, const char* rw)
    {
        f = fopen(n,rw);
        if (f==0)
            throw Open_failure(n);
    }

    ~File_handle()
    {
        fclose(f);
    }

private:
    File_handle(const File_handle&);
    File_handle& operator=(const File_handle&);

    FILE* f;
};

I've reformatted it slightly, as well as making it non-copyable (pretty important if you're writing it for real!). The class is a perfect example of RAII in action. If the object exists, the file it manages is open. This is a strong invariant.

Now, let's say I wanted to use make a new class which encapsulates a file, but that class had to fit into a framework which requires default construction:

class DefaultConstructible : public FrameworkClass
{
public:
    DefaultConstructible()
    : file_(???)
    {
    }

private:
    DefaultConstructible(const DefaultConstructible&);
    DefaultConstructible& operator=(const DefaultConstructible&);

    File_handle file_;
};

We have no arguments to construct the file_ member with. You may think to pre-set some global or class-static variables in advance of creating the DefaultConstructible object, but that would be a concurrency disaster waiting to happen, as well as proving yourself to be morally bankrupt. So we make use of the first of the two suggestions to delay creation of our member until a point where we are able to provide the arguments. boost::optional:

class DefaultConstructible : public FrameworkClass
{
public:
    DefaultConstructible()
    // File_handle is 'unconstructed' here
    {
    }

    void Initialise(const char* n, const char* rw)
    {
        file_ = boost::in_place(n, rw); // Construct the file_ properly here
    }

private:
    DefaultConstructible(const DefaultConstructible&);
    DefaultConstructible& operator=(const DefaultConstructible&);

    boost::optional<File_handle> file_;
};

If our File_handle class was copy/move constructible, we could have just used direct assignment to initialise it:

file_ = File_handle(n, rw);

However, it isn't, so we utilise another Boost utility called boost::in_place in order to allow the optional to directly construct the File_handle, rather than needing to a copy of a temporary one.

At this point you may ask, why not just 'new' the File_handle when we need it? For example:

class DefaultConstructible : public FrameworkClass
{
public:
    DefaultConstructible()
    // File_handle is 'unconstructed' here
    {
    }

    void Initialise(const char* n, const char* rw)
    {
        file_.reset(new File_handle(n, rw)); // Construct the file_ properly here
    }

private:
    DefaultConstructible(const DefaultConstructible&);
    DefaultConstructible& operator=(const DefaultConstructible&);

    std::auto_ptr<File_handle> file_;
};

Well, that's an equally valid option too. Generally, I am of the opinion that heap allocation is to be avoided where necessary. There's no need to allocate here, so I don't.

However, you may already be doing almost exactly this because you are employing what I mentioned as my second option: the Pimpl idiom. In which case, you're already most of the way there. All you need to do is ensure that you handle the error where your object isn't yet created before you forward on your calls to the implementation:

////////////////////////////
// DefaultConstructible.h //
////////////////////////////
class File_Handle;

class DefaultConstructible : public FrameworkClass
{
public:
    DefaultConstructible();
    ~DefaultConstructible();

    void Initialise(const char* n, const char* rw);

    void Read(const void* p, size_t bytes);

private:
    DefaultConstructible(const DefaultConstructible&);
    DefaultConstructible& operator=(const DefaultConstructible&);

    std::auto_ptr<File_handle> file_; // Or std::unique_ptr if you're now on C++11
};

//////////////////////////////
// DefaultConstructible.cpp //
//////////////////////////////
#include "File_handle.h"

DefaultConstructible:: DefaultConstructible() {}
DefaultConstructible::~DefaultConstructible() {}

void DefaultConstructible::Initialise(const char* n, const char* rw)
{
    assert(!file_.get());

    file_.reset(new File_handle(n, rw)); // Construct the file_ properly here
}

void DefaultConstructible::Read(const void* p, size_t bytes)
{
    assert(file_.get());

    file_->Read(p, bytes); // Assuming File_handle has a Read function
}

I have used assert here, but you can use whatever you like: relying on std::auto_ptr to do the check for you, error codes, exceptions or something more cunning.

1 comment: