11

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;

public:
    void Operation1(...);
    int Operation2(...);
    string Operation3(...);

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

This is not really the singleton pattern. Nothing is static and there may be many Thing instance, all managing their own legacy resources. Furthermore, this is not merely a smart-pointer. The wrapped pointer, _legacy is private and all operations are exposed via some public instance functions that hide the legacy API from consumers.

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, using malloc() as a place-holder for the code that would invoke the legacy API ...

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, again, free() is just a placeholder ...

    ~Thing() noexcept
    {
        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 and move-assignment-operator as follows...

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

    Thing& operator= (Thing&& old) noexcept
    {
        if (&old != this)
        {
            swap(_legacy, old._legacy);
        }
        return (*this);
    }

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!

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 and move-assignment-operator 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?

Xharlie
  • 2,350
  • 3
  • 19
  • 39
  • You could always use the [static singleton pattern](http://stackoverflow.com/questions/1008019/c-singleton-design-pattern) to avoid using smart pointers. – NathanOliver Sep 24 '15 at 12:54
  • It isn't really a singleton - only a pseudo-singleton. There might be several things, all acquired separately but managed by a `Thing` instance each. – Xharlie Sep 24 '15 at 13:06
  • It seems you are reinventing a smart pointer and then storing it in another smart ointer. I would just write a custom deleter and use `std::unique_ptr` or `std::shared_ptr` directly. – Galik Sep 24 '15 at 13:13
  • I have edited the title and question, hopefully this will clear things up. – Xharlie Sep 24 '15 at 13:22
  • 3
    Your code is fine, it doesn't get much better than that. I would allow self-move-assignment and make it a noop (surprised `one = move(one);` gives a build error), but that usually is not necessary. – nwp Sep 24 '15 at 13:31
  • 3
    The second and final question are better suited for [CodeReview](http://codereview.stackexchange.com). – dyp Sep 24 '15 at 14:26
  • I don't quite understand what kind of answer your third question is expecting. Are you referring to the non-elided move-construction when using `make_unique` and `make_shared`, or is this a general question about when a move-assignment operator is invoked (usually)? – dyp Sep 24 '15 at 14:36
  • `one = move(one);` isn't giving a build error any more and I can't seem to reproduce it. Perhaps it was a phantom. I also answered my question about the move-assignment operator so that has been scratched from the question. @nwp: How do you make self-move-assignment a noop? As it stands, my move-assignment operator works quite fine for self-assignment but I am really interested in all these nuances. – Xharlie Sep 24 '15 at 14:45
  • 2
    You would put `if (&old == this) return *this;` in the beginning of the assignment operator. In your case it doesn't really buy you much, but if you need to swap more than a pointer it may be worth it. I recommend you read [Effective Modern C++](http://www.oreilly.com/free/effective-modern-c++.html), because it talks about all those things in detail. – nwp Sep 24 '15 at 15:09
  • @nwp: As it turns out, the real-life class does have several data members that need to be `move()`d so I have modified my move-assignment operator according to your suggestions. (Thanks!) It's a personal preference but I always like to wrap the body of a function in an if-block if the if-predicate does not affect the return statement so I have used `if (&old != this) { // swaps }`. I assume they are equivalent. I also assume I can use the identical technique to short-circuit copy-assignment operators in other classes. – Xharlie Sep 24 '15 at 18:24
  • In case anyone is interested, I have just asked another question that is the direct sequel to this one: http://stackoverflow.com/questions/32808479/c-raii-pattern-and-named-destructors – Xharlie Sep 27 '15 at 13:38

2 Answers2

6

You should wrap your Thing in a smart pointer, then you won't need to worry about the copy and move semantics.

class Thing
{
private:
    void* _legacy;

public:
    void Operation1(...);
    int Operation2(...);
    string Operation3(...);

    Thing(const Thing&) = delete;
    Thing(Thing&&) = delete;
    Thing& operator=(const Thing&) = delete;
    Thing& operator=(Thing&&) = delete;

    static std::shared_ptr<Thing> acquire() {
        return std::make_shared<Thing>();
    }

private:
    Thing() : _legacy(malloc(16)) {
         // ...
    }

    ~Thing() {
        free(_legacy);
    }
};

Similarly, you can do it with unique_ptr:

std::unique_ptr<Thing> acquire() {
    return std::make_unique<Thing>();
}

You seemed to imply that you want to have only one instance of this thing, though even in your solution you did not attempt to do anything like that. For that you need static variables. Though remember that in this case your resource will only be freed after your main() function exits. For example:

static std::shared_ptr<Thing> acquire() {
    static std::shared_ptr<Thing> instance;
    if (!instance) {
        instance = std::make_shared<Thing>();
    }
    return instance;
}

Or the unique_ptr version:

static Thing& acquire() {
    static std::unique_ptr<Thing> instance;
    if (!instance) {
        instance = std::make_unique<Thing>();
    }
    return *instance;
}

Or, you can use weak_ptr to acquire one instance program-wide, which is freed when nobody uses it. In this case you won't be able to use unique_ptr for this purpose. This version will recreate the object if it was freed then needed again.

static std::shared_ptr<Thing> acquire() {
    static std::weak_ptr<Thing> instance;
    if (instance.expired()) {
        instance = std::make_shared<Thing>();
    }
    return instance.lock();
}
petersohn
  • 11,292
  • 13
  • 61
  • 98
  • 3
    @Xharlie But the principle that you can supply a custom deleter to the smart pointer is still valid. – Galik Sep 24 '15 at 13:12
  • Fair enough. Custom deleter is noted. But, ultimately, the class will have other functions on it that aren't just allocation and deallocation. – Xharlie Sep 24 '15 at 13:15
  • @Xharlie Then you may create the `Thing` class with whatever operations that does the `malloc()` thing in its constructor and the `free()` in the destructor, then put it in your `shared_ptr` or `unique_ptr` as I described. Then you won't need the custom deleters, and you can even use `make_shared` or `make_unique`. – petersohn Sep 24 '15 at 13:21
  • I have edited the title and question, hopefully this will clear things up. – Xharlie Sep 24 '15 at 13:21
  • If you define `acquireThing()` to return either `shared_ptr` or `unique_ptr`, you are forcing the consumer to use those. My goal was to allow the consumer the freedom to use the class as a local or with any smart-pointer type, as they see fit, but still prevent copying of the object. – Xharlie Sep 24 '15 at 13:24
  • I edited my answer to match your question. Anyway, if you are returning with `unique_ptr`, then you allow your user to convert it to `shared_ptr`, as `shared_ptr` has a constructor that accepts `unique_ptr&&` as a parameter, and take ownership of the contained object. But if you really want a singleton-like behavior, then I recommend the `shared_ptr`-`weak_ptr` combination. In this case though you need the `shared_ptr` functionality because you really have shared ownership. – petersohn Sep 24 '15 at 13:36
  • @dyp Of course, I intended it to be static, just forgot to write it out. Corrected, thanks. – petersohn Sep 24 '15 at 14:34
  • Guideline: Never delete your move members. At best deleted move members are a harmless, redundant distraction (as is in your code). I.e. they are not necessary in your code, the deleted copy members get the job done. At worst, deleted move members don't do what the beginner reader thinks they do (if you want the type to be copyable). – Howard Hinnant Sep 24 '15 at 15:22
  • @HowardHinnant Sometimes it is desirable to disallow moving. For example, as in our case, when we expect the object to be passed in a `shared_ptr`. We don't want it to suddenly lose its value. Besides if we delete the copy members but do not declare the move members, it results in the same non-copyable non-movable semantics. My guideline is to write out either all 4 or none of the copy-move members. – petersohn Sep 25 '15 at 08:55
2
struct free_thing{
  void operator()(void* p)const{
    // stuff
    free(p);
  }
};
using upthing=std::unique_ptr<void,free_thing>;

upthing make_thing(stuff){
  void*retval;
  // code
  return upthing(retval);
}

Store a upthing in your Thing as _legacy. Use default dtor, move ctor, move assign for Thing (=default).

The destroy code goes in free_thing.

Your ctor creates the upthing.

Now users can treat your Thing as a move only value type.

Do not write your own pointer managers unless you really need to. Unique and shared do lots for you: if you write your own smart pointer, use them as internal guts even.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I do like that. What happens if the destroy-code throws an exception? Is it allowed to throw exceptions? – Xharlie Sep 27 '15 at 13:45