16

Why and when do I need to supply my own deleter? Isn't keyword delete sufficient enough?

If you use a smart pointer to manage a resource other than memory allocated by new, remember to pass a deleter.


Update:

As being asked in comments, the reason why I am unclear about the quoted text and example is that I was thinking wrong about something, which it's that I had been thinking of smart pointer is only invented for/related to dynamic memory management. So the example uses smart pointer to manage a non-dynamic memory stuff makes me confused.

A good explanation from a senior:

The smart pointer doesn't care at all about something being dynamic memory as such. It's just a way to keep track of something while you need it, and destroy that something when it goes out of scope. The point of mentioning file handles, network connections, etc., was to point out that they're not dynamic memory, but a smart pointer can manage them just fine anyway.


C++ Primer 5th is taking a pseudo network connection (do not define destructors) to illustrate.

Bad:

struct destination; // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination*); // open the connection
void disconnect(connection); // close the given connection
void f(destination &d /* other parameters */)
{
// get a connection; must remember to close it when done
connection c = connect(&d);
// use the connection
// if we forget to call disconnect before exiting f, there will be no way to closes
}

Good:

void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}

Full context screenshot (I clear some unrelated text):
Smart Pointers and Dumb classes

Smart Pointers and Dumb classes part 2

Rick
  • 7,007
  • 2
  • 49
  • 79
  • 1
    `delete` is for memory deallocation, the opposite of `new`. However this is not talking about memory allocated with `new` but "a resource other than memory allocated by `new`" which means that `delete` is not appropriate for deallocating the resource as `new` was not used to create/generate/allocate the resource. – Richard Chambers Jul 11 '18 at 06:03
  • 1
    for who is the best user to know how to free or clean-up a resource than the one using it? – Joseph D. Jul 11 '18 at 06:07
  • 1
    If for example you wanted to store a database handle that you get from a function called `get_database_handle()`, there would probably be a counterpart function called `release_database_handle()` that you need to use. `delete` would do entirely the wrong thing. – BoBTFish Jul 11 '18 at 06:08
  • 1
    @RichardChambers ... and object destruction. – Paul Sanders Jul 11 '18 at 06:21
  • So, what is unclear about the example you've included in your question? Doesn't it explain this well enough? – BartoszKP Jul 11 '18 at 08:09
  • Even without the example, what do you find unclear about "a resource other than memory allocated by `new`"? – OrangeDog Jul 11 '18 at 12:32
  • @OrangeDog, Bartosz I don't think he is unclear, anymore. OP? – Paul Sanders Jul 12 '18 at 13:28
  • 1
    You can still allocate dynamic memory without using `new` - the most obvious is `malloc`, for which you would need to supply `free` as the deleter. – OrangeDog Jul 12 '18 at 13:34

4 Answers4

9

You need to supply your own delete to the smart pointer creation when the standard delete is not appropriate for deallocating, releasing, discarding, or otherwise disposing of a resource whose life time is being governed by the smart pointer.

A typical use of a smart pointer is to allocate memory as the resource being managed by the smart pointer so that when the smart pointer goes out of scope, the resource being managed, in this case memory, is discarded by using the delete operator.

The standard delete operator does two things: (1) call the object's destructor to allow the object to do any cleanup it needs to do before it's allocated memory is released or deallocated and (2) releases the memory that was allocated by the standard new operator for the object when it was constructed. This is the reverse order of what happens with the new operator which (1) allocates memory for the object and does the basic initialization needed to establish the construction environment for the object and (2) calls the constructor of the object to create the object's starting state. See What does the C++ new operator do other than allocation and a ctor call?

So the key question for needing your own deleter is "what actions that were done before the object constructor was invoked need to be unwound and backed out after the object's destructor completes?".

Normally this is a memory allocation of some kind such as is done by the standard new operator.

However in the case of some resource other than memory allocated with the new operator, the use of the delete operator is not appropriate since the resource is not memory that was allocated by using the new operator.

So when using a smart pointer for this kind of resource where the delete operator is not appropriate, you need to supply your own deleter method or function or operator which the smart pointer will use when it goes out of scope and triggers its own destructor which will in turn handle the discarding of any resources that are being managed by the smart pointer.

A simple example with output

I put together a simple example with std::unique_ptr<> along with the output generated to show using and not using a deleter with the pointer as well as showing explicit use of the destructor.

The source code of a simple Windows console application looks like:

// ConsoleSmartPointer.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <memory>
#include <string>
#include <iostream>

class Fred {
public:
    Fred() { std::cout << "  Fred Constructor called." << std::endl; }
    ~Fred() { std::cout << "  Fred Destructor called." << std::endl; }
};
class George {
public:
    George() { std::cout << "   George Constructor called" << std::endl; }
    ~George() { std::cout << "   George Destructor called" << std::endl; }
private:
    int iSomeData;
    std::string  a_label;
    Fred  myFred;
};

void cleanupGeorge(George *)
{
    // just write out a log and do not explicitly call the object destructor.
    std::cout << "  cleanupGeorge() called" << std::endl;
}

void cleanupGeorge2(George *x)
{
    // write out our message and then explicitly call the destructor for our
    // object that we are the deleter for.
    std::cout << "  cleanupGeorge2() called" << std::endl;
    x->~George();    // explicitly call destructor to do cleanup.
}

int func1()
{
    // create a unique_ptr<> that does not have a deleter.
    std::cout << "func1 start. No deleter." << std::endl;

    std::unique_ptr<George> p(new George);

    std::cout << "func1 end." << std::endl;
    return 0;
}

int func2()
{
    // create a unique_ptr<> with a deleter that will not explicitly call the destructor of the
    // object created.
    std::cout << "func2 start. Special deleter, no explicit destructor call." << std::endl;

    std::unique_ptr<George, void(*)(George *)> p(new George, cleanupGeorge);

    std::cout << "func2 end." << std::endl;
    return 0;
}

int func3()
{
    // create a unique_ptr<> with a deleter that will trigger the destructor of the
    // object created.
    std::cout << "func3 start. Special deleter, explicit destructor call in deleter." << std::endl;

    std::unique_ptr<George, void(*)(George *)> p(new George, cleanupGeorge2);

    std::cout << "func3 end." << std::endl;
    return 0;
}

int main()
{
    func1();
    func2();
    func3();
    return 0;
}

The above simple application generates the following output:

func1 start. No deleter.
  Fred Constructor called.
   George Constructor called
func1 end.
   George Destructor called
  Fred Destructor called.
func2 start. Special deleter, no explicit destructor call.
  Fred Constructor called.
   George Constructor called
func2 end.
  cleanupGeorge() called
func3 start. Special deleter, explicit destructor call in deleter.
  Fred Constructor called.
   George Constructor called
func3 end.
  cleanupGeorge2() called
   George Destructor called
  Fred Destructor called.

Additional posts

What is a smart pointer and when should I use one?

Using custom deleter with std::shared_ptr

See as well this discussion about deleter with std::make_shared<> and why it isn't available. How to pass deleter to make_shared?

Is custom deleter for std::unique_ptr a valid place for manual call to destructor?

When does std::unique_ptr<A> need a special deleter if A has a destructor?

RAII and smart pointers in C++

Richard Chambers
  • 16,643
  • 4
  • 81
  • 106
  • 4
    Freeing non-memory resources is the job of the destructor. The point is that not all classes have well-behaved destructors (cited from the primer). If we can't fix these classes and still want to manage their life time with smart pointers, then we should resort to a custom deleter. The other use case is when the object has not been allocated by standard `new` as mentioned in @Paul Sander's answer (and also given as example in the primer). – Adrian W Jul 11 '18 at 08:20
5

When (obviously) delete is not the way you want to destroy the object. An object allocated with placement new might be a simple example.

The example from the primer is actually quite good (I owe them one after trashing them earlier), but another creative use of std::shared_ptr (or std::unique_ptr) might be to manage the lifetime of a COM object. These are deallocated by calling their Release () method, rather than by calling delete (and if you did that, well, goodnight Vienna).

So, just to illustrate that, you might do:

static void release_com_object (IUnknown *obj) { obj->Release (); }

IUnknown *my_com_object = ...
std::shared_ptr <IUnknown> managed_com_object (my_com_object, release_com_object);

You don't need to know anything about COM to understand the underlying idea here. There are, in general, any number of ways to release a resource and a suitable set of custom deleters can handle them all, it's a really cool trick.


Ah, I'm really getting into the groove now. Here's another one for you, this time with std::unique_ptr and a lambda (don't know why they're using a shared_ptr there - it's a lot more expensive). Note the different syntax when using std::unique_ptr - you have to tell the template the function signature of the deleter:

FILE *f = fopen ("myfile", "r");

if (f)
{
    std::unique_ptr <FILE, void (*) (FILE *)> (f, [] (FILE *f) { fclose (f); });
    // Do stuff with f
}   // file will be closed here

Oh my, so much you can do.

Live demo.

Smi
  • 13,850
  • 9
  • 56
  • 64
Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • Using a `shared_ptr` to manage a COM object feels like overkill to me... – Neil Jul 11 '18 at 11:42
  • @Neil I agree. It should of course be a `unique_ptr`. I just went with the flow there, given what the OP said in his question. Please see my subsequent supplementary example where I do indeed use a `unique_ptr`. – Paul Sanders Jul 11 '18 at 12:40
3

The example demonstrates how to leverage the deterministic lifetime of instances of a type. What happens upon their destruction is defined by the destructor (rules out the built-in types, they don't have one). The destructor is the part of the type that "cleans up" its state. While there is often not much to do, memory allocations indeed have to be cleaned up, and in the example, a disconnect function must be invoked. This is true for any type that manually manages resources (other than simple aggregation or aquaintance of member variables), and the example could equally well be

class ConnectionHandle {
    public:
        ConnectionHandle(destination& d) : c(connect(d)) {}
        ~ConnectionHandle() { end_connection(c); }
    private:
        connection& c;
};

When the lifetime of such a type shall be managed by a smart pointer, it's one possibility to use the destructor of the smart pointer to clean up the resources, and that's what the exapmle is about. This works for std::shared_ptr as well as std::unique_ptr, though in the latter case, the custom deleter is part of the signature of the type (more typing when passing the unique_ptr around).

It is also instructive to compare this situation with those where no custom deleter is necessary:

struct A { int i; std::string str; };

auto sp = std::make_shared<A>(42, "foo");

Here the resources of A are values owned by A ("aggregation"), cleanup happens automatically (nothing to do for i, str is managed by std::string::~string()).

lubgr
  • 37,368
  • 3
  • 66
  • 117
2

C++ allows you to write your own custom allocators with new. Just like how you should delete everything you new, you should have everything allocated by your custom allocator also be deleted by it.

A concrete example of an issue caused by this would be if you use a custom allocator to track memory budgets (i.e. you assign every allocation to some budget, and present a warning when you exceed any of those budgets). Let's say this wraps new and delete, so when your smart pointer goes out of scope, only delete gets called, and custom allocator has no idea that memory was freed, and you end up with inaccurate memory usage for your budgets.

If that same type of wrapping allocator was used to detect leaks, calling delete directly would cause a false positive.

If you're actually allocating your own memory manually for whatever reason, then you're going to have a very bad time when delete tries to free it.

In the case of your example, the network connection's memory is freed without first being able to cleanly disconnect. The result of that in a real-world case might be that the other end of the connection hangs until it times out, or presents some sort of error about a dropped connection.

Robert Rouhani
  • 14,512
  • 6
  • 44
  • 59