Jonathan Boccara's blog

Smart developers use smart pointers (3/7) – Custom deleters

Published August 29, 2017 - 14 Comments

Daily C++

The previous episodes of the series explained what smart pointers are, and which ones to use in your code. Here I show a technique that allows to encapsulate complex memory management into std::unique_ptr, to relieve your code from low-level memory manegement.

The series Smart developers use smart pointers contains:

Motivation

The use case here is a class using a polymorphic class.

Let’s take the example of a House class, that carries its building Instructions with it, which are polymorphic and can be either a Sketch or a full-fledged Blueprint:

One way to deal with the life cycle of the Instructions is to store them as a unique_ptr in the House. And say that a copy of the house makes a deep copy of the instructions:

class House
{
public:
    explicit House(std::unique_ptr<Instructions> instructions)
        : instructions_(std::move(instructions)) {}
    House(House const& other)
        : instructions_(other.instructions_->clone()) {}

private:
    std::unique_ptr<Instructions> instructions_;
};

Indeed, Instructions has a polymorphic clone, which is implemented by the derived classes:

class Instructions
{
public:
    virtual std::unique_ptr<Instructions> clone() const = 0;
    virtual ~Instructions(){};
};

class Sketch : public Instructions
{
public:
    std::unique_ptr<Instructions> clone() const { return std::unique_ptr<Instructions>(new Sketch(*this)); }
};

class Blueprint : public Instructions
{
public:
    std::unique_ptr<Instructions> clone() const { return std::unique_ptr<Instructions>(new Blueprint(*this)); }
};

As a passing note, there would be much to say about polymorphic clones. But we get real deep into them towards the end of the series on smart pointers. No spoilers.

Here is a way to construct a house:

enum class BuildingMethod
{
    fromSketch,
    fromBlueprint
};

House buildAHouse(BuildingMethod method)
{
    if (method == BuildingMethod::fromSketch)
        return House(std::unique_ptr<Instructions>(new Sketch));
    if (method == BuildingMethod::fromBlueprint)
        return House(std::unique_ptr<Instructions>(new Blueprint));
    throw InvalidBuildMethod();
}

where the building method may come from user input.

The situations gets technically much more challenging when objects can come from another memory source, like the stack for example:

Blueprint blueprint;
House house(???); // how do I pass the blueprint to the house?

Indeed, we can’t bind a unique_ptr to a stack-allocated object, because calling delete on it would cause undefined behaviour.

One solution would be to make a copy of the blueprint and allocating it on the heap. This may be OK, or it may be costly (I’ve come across a similar situation once where it was the bottleneck of the program).

But anyway, the need is totally legitimate to want to pass objects allocated on the stack. The thing is, we just don’t want the House to destroy the Instructions in its destructor when the object comes from the stack.

How can std::unique_ptr help here?

Seeing the real face of std::unique_ptr

Most of the time, the C++ unique pointer is used as std::unique_ptr<T>. But its complete type has a second template parameter, its deleter:

template<
    typename T,
    typename Deleter = std::default_delete<T>
> class unique_ptr;

std::default_delete<T> is a function object that calls delete when invoked. But it is only the default type for Deleter, and it can be changed for a custom deleter.

This opens the possibility to use unique pointers for types that have a specific code for disposing of their resources. This happens in legacy code coming from C where a function typically takes care of deallocating an object along with its contents:

struct GizmoDeleter
{
    void operator()(Gizmo* p)
    {
        oldFunctionThatDeallocatesAGizmo(p);
    }
};

using GizmoUniquePtr = std::unique_ptr<Gizmo, GizmoDeleter>;

(By the way, this technique is quite helpful as a step to simplify legacy code, in order to make it compatible with std::unique_ptr.)

Now armed with this feature, let’s go back to our motivating scenario.

Using several deleters

Our initial problem was that we wanted the unique_ptr to delete the Instructions, except when they came from the stack in which case we wanted it to leave them alone.

The deleter can be customized to delete or not delete, given the situation. For this we can use several deleting functions, all of the same function type (being void(*)(Instructions*)):

using InstructionsUniquePtr = std::unique_ptr<Instructions, void(*)(Instructions*)>;

The deleting functions are then:

void deleteInstructions(Instructions* instructions){ delete instructions;}
void doNotDeleteInstructions(Instructions* instructions){}

One deletes the object, and the other doesn’t do anything.

To use them, the occurrences of std::unique_ptr<Instructions> needs to be replaced with InstructionUniquePtr, and the unique pointers can be constructed this way:

if (method == BuildingMethod::fromSketch)
    return House(InstructionsUniquePtr(new Sketch, deleteInstructions));
if (method == BuildingMethod::fromBlueprint)
    return House(InstructionsUniquePtr(new Blueprint, deleteInstructions));

Except when the parameter comes from the stack, in which case the no-op deleter can be used:

Blueprint blueprint;
House house(InstructionsUniquePtr(&blueprint, doNotDeleteInstructions));

EDIT: as iaanus pointed out on Reddit, we should note that this is a dangerous technique. Indeed, the unique_ptr can be moved out of the scope of the stack object, making it point to a resource that doesn’t exist any more. Using the unique_ptr after this point causes a memory corruption.

And, like Bart noted in the comment section, we should note that if the constructor of House were to take more than one argument then we should declare the construction of the unique_ptr in a separate statement, like this:

InstructionsUniquePtr instructions(new Sketch, deleteInstructions);
return House(move(instructions), getHouseNumber());

Indeed there could be a memory leak if an exception was thrown. You can read all about this classical pitfall in Item 17 of Effective C++.

And also that when we don’t use custom deleters, we shouldn’t use new directly, but prefer std::make_unique that lets you pass the arguments for the construction of the pointed-to object.

Thanks to Bart and iaanus for their valuable contributions. – end EDIT

Safety belt

Now if we’re very careful and you avoid memory corruptions, using a custom deleter solves the initial problem but it induces a little change in the semantics of the passed argument, that can be at the source of many bugs.

Indeed in general, holding an std::unique_ptr means being its owner. And this means that it is OK to modify the pointed-to object. But in the case where the object comes from the stack (or from wherever else when it is passed with the no-op deleter), the unique pointer is just holding a reference to an externally owned object. In this case, you don’t want the unique pointer to modify the object, because it would have side effects on the caller. Allowing this makes things more complicated.

For this reason,when using this technique I recommend to work on pointer to const objects:

using InstructionsUniquePtr = std::unique_ptr<const Instructions, void(*)(const Instructions*)>;

and the deleters become:

void deleteInstructions(const Instructions* instructions){ delete instructions;}
void doNotDeleteInstructions(const Instructions* instructions){}

This way, the unique pointer can’t cause trouble outside of the class. This will save you a sizable amount of debugging.

Overall, I hope this technique can be helpful for you.

But really, when you think about it, all this code is complicated. Even if the requirements are really simple: using objects coming from the stack or from the heap, and not blowing up everything. This requirement ought to have a simple implementation in code, but see what we had to do to ensure it works. Despite my deep love for C++, I think that other languages, like Java or C#, would do better in this situation. Or I’ve missed something.

Your impressions are welcome on this.

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed