jeudi 24 septembre 2015

C++ Move Semantics and pseudo-singletons

I am working with a legacy C API under which acquiring some resource is expensive and freeing that resource is absolutely critical. I am using C++14 and I want to create a class to manage these resources. Here is the basic skeleton of the thing...

class Thing
{
private:
    void* _legacy;

private:
    Thing(void* legacy) :
        _legacy(legacy)
    {
    }
};

The constructor is private because instances of Thing will be returned from a static factory or named-constructor that will actually acquire the resource. Here is a cheap imitation of that factory...

public:
    static Thing Acquire()
    {
        // Do many things to acquire the thing via the legacy API
        void* legacy = malloc(16);

        // Return a constructed thing
        return Thing(legacy);
    }

Here is the destructor which is responsible for freeing the legacy resource...

    ~Thing()
    {
        if (nullptr != _legacy)
        {
            // Do many things to free the thing via the legacy API
            // (BUT do not throw any exceptions!)
            free(_legacy);
            _legacy = nullptr;
        }
    }

Now, I want to ensure that exactly one legacy resource is managed by exactly one instance of Thing. I did not want consumers of the Thing class to pass instances around at will - they must either be owned locally to the class or function, either directly or via unique_ptr, or wrapped with a shared_ptr that can be passed about. To this end, I deleted the assignment operator and copy constructors...

private:
    Thing(Thing const&) = delete;
    void operator=(Thing const&) = delete;

However, this added an additional challenge. Either I had to change my factory method to return a unique_ptr<Thing> or a shared_ptr<Thing> or I had to implement move semantics. I did not want to dictate the pattern under which Thing should be used so I chose to add a move-constructor, as follows...

    Thing(Thing&& old) : _legacy(old._legacy)
    {
        // Reset the old thing's state to reflect the move
        old._legacy = nullptr;
    }

With this all done, I could use Thing as a local and move it about...

    Thing one = Thing::Acquire();
    Thing two = move(one);

I could not break the pattern by attempting to commit self-assignment:

    Thing one = Thing::Acquire();
    one = one;                      // Build error!
    one = move(one);                // Build error!

I could also make a unique_ptr to one...

    auto three = make_unique<Thing>(Thing::Acquire());

Or a shared_ptr ...

    auto three = make_shared<Thing>(Thing::Acquire());

Everything worked as I had expected and my destructor ran at exactly the right moment in all my tests. In fact, the only irritation was that make_unique and make_shared both actually invoked the move-constructor - it wasn't optimized away like I had hoped.

First Question: Have I implemented the move-constructor correctly? (They're fairly new to me and this will be the first time I have used one in anger.)

Second Question: Please comment on this pattern! Is this a good way to wrap legacy resources in a C++14 class?

Finally: Should I change anything to make the code better, faster, simpler or more readable?

Aucun commentaire:

Enregistrer un commentaire