Who owns the memory?

Author: Chloé Lourseyre
Editor: Peter Fordham

Have you ever heard of “ownership” of memory, in C++, when speaking about pointers?

When you use raw pointers, you have to delete them (or else, you’ll leak memory). But if the said pointer is passed down functions and complex features, or returned by a factory, you have to know whose job it is to delete.

Ownership means “responsibility to cleanup”. The owner of the memory is the one who has to delete its pointer.

Deletion can either be explicit (through the keyword delete of the function free() regarding raw pointers) or bound to the lifetime of an object (through smart pointers and RAII1).

In this article, the term “pointer” will refer to both raw and smart pointers.

The problem of memory ownership

The problem is addressed by this post: You Can Stop Writing Comments About Pointer Ownership (gpfault.net).

TL;DR: smart pointer allows you to cover every case that could have used raw pointers, so don’t use raw pointers. Move semantics on smart pointers allow ownership rules to be checked at compile time.

The post is quite interesting, but misses a huge point: what do we do with existing raw pointers? What do we do when we are forced to use raw pointers2?

In the rest of the article, these are the questions I will try to answer.

First, when you have to use a feature that requires raw pointers, you have to ask yourself: how does the feature behave regarding the ownership of the resources?

Once that is done, we can enumerate four cases:

  • When you receive a pointer and the ownership
  • When you receive a pointer but not the ownership
  • When you transfer a pointer but not the ownership
  • When you transfer a pointer and the ownership

When you receive a pointer and the ownership

This one’s probably the easiest. Since we can construct a std::unique_ptr or std::shared_ptr using a raw pointer, all we have to do is put the received raw pointer into any smart pointer and it will be properly destroyed.

Example

#include <memory>
#include <iostream>

struct Foo
{
    Foo() { std::cout << "Leak?" << std::endl; }
    ~Foo() { std::cout << "No leak" << std::endl; }
};

// We don't own this function, so we can't change the return value
Foo * make_Foo()
{
    return new Foo();
}

int main()
{
    std::unique_ptr<Foo> foo_ptr(make_Foo());
    // The instance of Foo is properly destroyed when function ends
    return 0;
}

The resulting output will be:

Leak?
No leak

When you receive a pointer but not the ownership

This case is a bit trickier. It does not happen often, but sometimes, for internal reasons (or just because it is badly conceived), a library gives you a pointer you must not delete.

Here, we can not use a smart pointer (like in the previous case) because it will delete the pointer.

For instance, in the following example, the class IntContainer creates a pointer to an int and deletes it when it is destroyed:

// We don't own this container, we can't change its interface
struct IntContainer
{
    IntContainer(): int_ptr(new int(0)) {}
    ~IntContainer() { delete int_ptr; }

    int * get_ptr() { return int_ptr; }

    int * int_ptr;
};

If we try to use an unique_ptr, like so:

int main()
{
    IntContainer int_cont;
    std::unique_ptr<int>(int_cont.get_ptr());
    // Double delete
    return 0;
}

We get undefined behavior. With my compiler (GCC 11.2) I got a runtime exception: free(): double free detected in tcache 2

There is a simple solution to this problem. Instead of using a pointer, we can get a reference to the object pointed by the raw pointer returned by IntContainer. This way, we can access the pointed value just as we can with a pointer, without risking deleting it.

int main()
{
    IntContainer int_cont;
    int & int_ref = *int_cont.get_ptr();
    // We have access to the value of int_ptr via the reference
    return 0;
}

When you transfer a pointer but not the ownership

Some libraries require that you give them raw pointers. In most cases, you keep the ownership of that pointers, but the problem of having to pass a raw pointer exists.

There are 2 situations:

  • You have the object as a value or a reference.
  • You have a smart pointer to the object.

Situation 1: You have the object as a value or a reference

In that situation, all you have to do is use the & operator to pass the address of your object down to the feature that requires a raw pointer. Since it won’t try to delete it, nothing bad will happen.

#include <iostream>

struct Foo
{
    Foo() { std::cout << "Leak?" << std::endl; }
    ~Foo() { std::cout << "No leak" << std::endl; }
};

// Function that requires a raw pointer
void compute(Foo *)
{
    // ...
}

int main()
{
    Foo foo;
    // ...
    compute(&foo);
    return 0;
}

Situation 2: You have a smart pointer to the value

When all you have is a smart pointer to the object, you can use the member function get() to get the raw pointer associated with the smart pointer. Both std::unique_ptr and std::shared_ptr implement this function3.

#include <memory>
#include <iostream>

struct Foo
{
    Foo() { std::cout << "Leak?" << std::endl; }
    ~Foo() { std::cout << "No leak" << std::endl; }
};

// Function that requires a raw pointer
void compute(Foo *)
{
    // ...
}

int main()
{
    std::unique_ptr<Foo> foo_ptr = std::make_unique<Foo>();
    // ...
    compute(foo_ptr.get());
    return 0;
}

When you transfer a pointer and the ownership

Probably the rarest case of all4, but hypothetically there could be a feature that requires a raw pointer and take ownership.

Situation 1: You have the object as a value or a reference

If you have the plain object (or a reference), the only way to make it a raw pointer that the feature will be able to delete safely is by calling new.

However, just a new will copy the object and this is unwanted. Since the ownership is theoretically given to the feature, we can do a std::move on the object to call the move constructor (if existent) and avoid a perhaps heavy copy of the object

So we just need to call the feature that requires a raw pointer, with a new to create the desired pointer, in which we move the value.

#include <iostream>

struct Foo
{
    Foo() { std::cout << "Leak?" << std::endl; }
    Foo(const Foo &&) { std::cout << "Move constructor" << std::endl; }
    ~Foo() { std::cout << "No leak" << std::endl; }
};

void compute(Foo *foo)
{
    // ...
    delete foo;
}

int main()
{
    Foo foo;
    // ...
    compute(new Foo(std::move(foo)));
}

Situation 2: You have a smart pointer to the value

The get() member function does not give ownership, so if we use it to pass the raw pointer to the feature, we’ll have double deletion.

The member function release(), however, releases the ownership and returns the raw pointer. This is what we want to use here.

#include <iostream>
#include <memory>

struct Foo
{
    Foo() { std::cout << "Leak?" << std::endl; }
    ~Foo() { std::cout << "No leak" << std::endl; }
};

void compute(Foo *foo)
{
    // ...
    delete foo;
}

int main()
{
    std::unique_ptr<Foo> foo_ptr = std::make_unique<Foo>();
    // ...
    compute(foo_ptr.release());
    return 0;
}

The problem is that release() is only a member of unique_ptr. Shared pointers can have multiple instances pointing to the same value, therefore they don’t own the pointer in the first place.

How do I know the intended ownership?

This is key when you do this kind of refactoring, because misidentification of the intended ownership may lead to memory leaks or undefined behavior.

Usually, the feature documentation will point out whose job it is to cleanup memory.

How to deal with memory allocated with malloc?

The cases that are presented in this article only concern memory that is allocated with new and freed with delete.

But there will be rare cases where the feature you want to use will have recourse to malloc and free.

When a feature asks for a raw pointer and you have to delete it, this problem is non-existent (you have control of the allocation and the cleanup)

When a feature gives you a raw pointer (created using malloc) and you mustn’t delete it, you have nothing to do since it’s not your job to free the resource. You can work with a reference to the resource as is demonstrated earlier.

When a feature asks for a raw pointer and you mustn’t delete it (because it uses free on it), you will have to do the malloc yourself. If you have a smart pointer on the object, you will unfortunately still have to use malloc.

Last, when a feature gives you a raw pointer (created using malloc) and you have to delete it, this is the tricky part. The best way to do this is probably to use a unique_ptr, with a custom deleter as second template. The second template parameter of unique_ptr is a function object (i.e. a class that implements operator()) that will be called to free the memory. In our specific case, the deleter we will need to implement will simply call free. Here is an example:

#include <memory>
#include <iostream>

struct Foo {};

// We don't own this function, so we can't change the return value
Foo * make_Foo()
{
    return reinterpret_cast<Foo*>(malloc(sizeof(Foo)));
}

// This deleter is implemented fo Foo specifically, 
// but we could write a generic template deleter that calls free().
struct FooFreer
{
    void operator()(Foo* foo_ptr)
    {
        free(foo_ptr);
    }
};

int main()
{
    std::unique_ptr<Foo, FooFreer> foo_ptr(make_Foo());
    // The instance of Foo is properly destroyed when function ends
    return 0;
}

Wrapping up

Here is a table summarizing what has been demonstrated here:

I receive a raw pointerI transfer a raw pointer
I must delete itStore it in a unique_ptr or in a shared_ptrUse the & operator or the .get() member function
I mustn’t delete itGet a reference to the objectUse the new operator and move or the .release() member function

With these tools, you can safely remove raw pointers from your code, even when some libraries still use them

The solutions proposed are very simple, but it is critical to identify which one to use in each situation. The main problem with this method is that the refactorer has to correctly identify ownership (but that’s a problem that cannot be avoided).

Thanks for reading and see you next time!

Author: Chloé Lourseyre
Editor: Peter Fordham

Addendum

Notes

  1. In case you didn’t know, RAII is an essential technique in modern C++, that is about resource acquisition and release. We often say that “something is RAII” when it cleans up perfectly and can not lead to memory leaks, as soon as it is released from the stack. For instance, raw pointers are not RAII because if you forget to delete them, you have a memory leak. On the contrary, std::string and std::vector are RAII because they release their internal allocations as soon as their destructor is called.
  2. It is sometimes hard for some developers to understand how can something can be “enforced” in your code. Here are a few situations when it occurs:
    – When you arrive on an existing project. You can’t refactor everything, on your own. You have to adapt and take you time to change things.
    – When parts of the code are not in your jurisdiction. On many projects, some essential code is developed by another team, in which you can’t meddle.
    – When you have to prioritize refactoring. You can’t always rewrite everything you’d like to at once. You have to choose your battles.
    – When management doesn’t give you approval or budget for the refactoring you are asking for. It happens, and there is nothing much you can do about it.
  3. No case presented in this article can work with a std::weak_ptr.
  4. While writing this article, I could not find, either on the Internet or in my memory, one single example of a feature that requires a pointer, then deletes it for you.

One thought on “Who owns the memory?”

Leave a Reply