Skip to the content.

Ownership

What I mean by “ownership”

Ownership is about lifetimes of objects and resources and who needs to manage them.

Ownership is a term that’s common in the “modern” C++ community, though the concepts it denotes are more broadly applicable.

Ownership is the responsibility of making sure that an object is created when it is needed, maintained while it is still necessary, and cleaned up exactly once when it is no longer needed, including freeing any other resources or whatever else it might need to do when destroyed.

I’m not sure if the term exists in other contexts, but ownership is a concept that exists outside of modern C++, it exists even when using older style C++ or C or even other languages; it is a fundamental part of a program having resources that it must manage.

The kinds of issues that can come from not thinking about ownership are:

RAII

Resource Acquisition Is Initialisation

Asking for a place to put a thing actually gives you the thing.

This means you cannot have uninitialised objects in C++.

This is funny, because every C++ developer knows it isn’t true, but it is true: In C++, built-in types declared with automatic storage and no explicit initialisation are initialised to indeterminate values, which is in practice the same thing as the values being uninitialised, but the standard calls this “default initialization”.

Class or struct types will have their constructor called, which will in turn initialise all its data members (recursively), using default initialisation unless explicitly initialised.

Ridiculous Acronym Isn’t Intuitive

RAII also refers to the fact that the object will be cleaned up as soon as it goes out of scope - the destructor will be called and the memory will be deallocated.

This feature of C++, combined with destructors and move semantics, makes it possible to have compiler-enforced representation of particular types of ownership in C++.

This is why, as far as I know, “ownership” is most commonly talked about in the “modern” C++ community.

Other languages and Garbage Collection

Other languages usually rely on a Garbage Collector to clean up objects which are no longer referenced by anything.

To C++ developers, garbage collectors have a bad reputation, but garbage collectors in general have advanced quite a long way and some are now quite good at not locking up the program while they run and not being susceptible to having reference cycles causing memory leaks.

However, garbage collected languages often don’t provide a mechanism that’s equivalent to C++’s RAII concept, where objects are guaranteed to be cleaned up in a deterministic and customisable way as soon as their scope is exited.

These other languages also tend not to have support for a destructor equivalent, which means managing resources that are not just memory allocations becomes tedious and potentially error prone.

Some garbage collected languages do have things to ensure objects are cleaned up at the end of a given function or scope, like defer (Go) or with (Python).

Rust has a “borrow checker” which sounds pretty swell but I don’t know Rust so ¯\_(ツ)_/¯. However, I think this keeps track of ownership from more of a thread-safety perspective than an more general ownership tracking perspective.

I believe Rust does have an RAII like mechanism and move semantics.

Garbage Collection and C++

I know third party garbage collectors exist for C++, but I don’t know what they are, how hard they are to use, or what their characteristics are.

As far as I know, there is no serious and widely supported intention to have Garbage Collection added to standard C++.

It would be swell to have a language that has good standard support for both objects managed by a standard garbage collector as well as objects managed by more explicit ownership modelling types like smart pointers and RAII mechanisms like C++ has, as long as any given object is owned by either only the garbage collector or only the smart pointer.

Smart pointers

Smart pointers are types which contain a pointer to an object, but are themselves an object following RAII principles, and which model ownership of the pointed to object.

Upon destruction of the smart pointer, the pointed-to object will be cleaned up if it is the last (compatible) smart pointer pointing to the object.

The clean up will involve calling the destructor of the pointed-to object, as well as deallocating the memory.

Unique Ownership

Unique ownership is a model of resource management where there is a single point of ownership for an object.

This is the most common situation: the object exists somewhere for a predictable period, and anything that needs to refer to the object can do so at a predictable time within the object’s lifetime, and can therefore safely do so without owning it.

Unfortunately, C++ doesn’t really offer a way to have non-owning reference semantics to a uniquely owned object in such a way that the compiler can check that the referred-to object is still valid at the time of access, though static analysers are often able to detect this.

As such, knowing whether using non-owning reference semantics to uniquely owned objects is safe is up to the programmer.

There are some fairly easy ways to make sure of this safety though:

It is preferable not to rely on static lifetime as a way to make sure accesses are safe as this basically has all the downsides of a global variable, and it doesn’t completely solve the lifetime issues:

Examples of modelling unique ownership in C++ include

Shared Ownership

Shared ownership is a model of resource management where there is multiple references to a single resource, and it is not possible to know at compile time which reference will be the last.

As such, the references themselves need to be tracked, and the resource being referred to should only be cleaned up when the last reference is clean up.

If it is possible to know at compile time which reference will be cleaned up last, it is more efficient to use a form of unique ownership, though there might be some rare situations where the ownership and lifetimes might be clearer with a type that models shared ownership and the ideally small performance impact might be worth avoiding bugs during maintenance in future.

An example of a situations where shared ownership is necessary or the best choice might be if you have multiple threads operating on a file, and you want to free the file when all the threads are finished, but you don’t know at compile time which thread will take the longest.

std::shared_ptr is the only type in standard C++ which models shared ownership, although third party and in-house equivalents also exist and are reasonably common, and there are many ways to model shared ownership outside of using this particular type.

Unlike with stdd::unique_ptr, it is possible in C++ to have non-owning references to objects which are owned by std::shared_ptrs and ensure all access to that object are safe and only within the lifetime of the referred-to object; std::weak_ptr was made for this purpose.

It is not possible to move ownership of an object from a std::shared_ptr to a std::unique_ptr, because in the general case, there is no safe way to ensure that there is only one std::shared_ptr pointing to an object.

Move Semantics

Move semantics is a term which refers to the concept of an object being passed from one location to another, and allowing C++ to optimise the operation.

This is necessary for performance in C++ because it is so common to use value types in C++, and objects are often non-trivial to construct, copy and destroy. It is common enough that one instance is no longer needed and another instance of the same value is needed elsewhere and as such C++11 standard introduced move semantics.

If an object owns memory that exists elsewhere, a move operation can simply pass ownership of that memory to the new object, whereas a deep copy would require another allocation and copying the data.

Copy-on-write is another solution to a similar set of problems, but it is not quite the same, and isn’t specifically supported by the C++ standard.

The addition of move semantics allowed C++ to implement std::unique_ptr as a replacement for the broken std::auto_ptr; now the compiler could enforce that a std::unique_ptr could never be copied and that all passing of a std::unqiue_ptr would result in the old copy being nulled out.

Move semantics are also available to std::shared_ptr so that ownership can be moved without incrementing and decrementing the atomic reference counter, while also ensuring that the old pointer gets nulled out.

Move semantics are also supported by most if not all of the standard library types where appropriate, such that moving a vector costs no more than copying 3 pointers, and similar with moving a string.

Improvements in the standard that came in C++11, C++14 and C++17 (often to catch up with optimisations that all major compilers have done for decades) have also meant that move semantics are not even necessary in a lot of cases:

Value types

Value types are either directly a local value, or an object that transparently represents a value and will do a deep copy by default.

A value type may be an object with automatic storage, or objects with external memory that’s encapsulated entirely by the type like a std::string or std::vector; when you copy one, it copies the contents too.

Knowing if a type manages memory elsewhere is important for performance characteristics and so on, but if it is written correctly and fully encapsulated and supports value semantics, then it is not important for ownership: all types value types represent ownership of their value.

C++ is an unusual language in that all types are value types by default (and passed-by-value) as it is more explicit for the programmer to control performance characteristics, but it can lead to some major performance issues if the programmer doesn’t realise this.

Value types can make runtime polymorphism difficult to achieve, but there are tools to help with this, such as std::variant.

Reference types

(Unfortunately, I am using the word “reference” here in it’s English sense, not in the C++ sense)

Reference types refer to an object that may be somewhere else rather than having it directly.

This may be via a pointer, a C++ reference, an iterator, a smart pointer object, an object that does not do deep copies by default, etc.

Reference types are required when you need to read or modify an object that exists somewhere else and is also often used when that isn’t necessary, but a value is expensive or not possible to construct, copy or destroy.

Reference types may or may not model ownership, it is up to the programmer to choose a reference type which models the kind of ownership that is appropriate for the situation.

Most popular languages use reference types (and pass-by-reference) by default, though their compiler or runtime may optimise these to become effectively the same as if they were value types. It’s very hard or impossible to force something to be a value in these languages and as such it can be very hard or impossible to give performance guarantees.

Reference types make reasonable performance easier to achieve naively, and also allows for easy polymorphism.

C++ allows reference types and pass-by-reference, but doing so is explicit.

Optional values

Sometimes a value, such as a function parameter or return value, can exist or not exist. Because of RAII, we need to have types which have a state that can represent no value, for the cases where the value does not exist.

Traditionally, a raw pointer is used to represent optional values, it is set to nullptr when it doesn’t exist, and a valid pointer to an object when it does exist.

A C++ reference cannot be used for optional semantics, because a C++ reference cannot be null (without invoking Undefined Behaviour).

Smart pointers, particularly the standard ones (std::shared_ptr and std::unique_otr), are able to represent optionality also; they can have a value of nullptr which represents that it is not pointing to an object. This is even their default constructed state.

C++17 introduced std::optional, and earlier some 3rd party libraries introduced equivalent types, which are basically a class which combines a flag and a value. The flag is set when the value is set, and the value can be retrieved from the optional object.

These types better represent what is happening to future readers of the code as you explicitly call out that the value is optional, whereas a raw pointer may be there for a number of different reasons. Raw pointers also necessitate an indirection, where std::optional and its equivalents are able to store the value internally. std::optional is therefore a value type, where a raw pointer is a reference type.

The types often have a mechanism for making sure that the value exists before allowing access to it.

Automatic Storage and the Free Store

Automatic storage is what the C++ standard calls “things that are on the stack”, except that it also includes things like direct non-static data members, which might be in the heap if their containing class instance is on the heap, like a member of an object in a std::vector.

Things in automatic storage are necessarily uniquely owned by the enclosing scope, whether that be a function or a class. This makes it a very strongly preferred default place to store objects, on top of the performance advantages of cache coherency, lack of indirection, and transparency to optimisers - this last point means that the compiler is allowed to put something which is in automatic storage into a different scope from which it is initialised - for example, in Return Value Optimisation.

Free store is what the C++ standard calls the heap. It uses the term “free store” because the C++ standard does not specify that the machine must have a stack + heap memory model, so uses this agnostic term to mean “wherever the runtime determines is an appropriate place to allocate things”.

Things in the free store need to have their ownership carefully managed by the programmer, preferably by using a type that models ownership.

Static storage

Static storage is uniquely owned like automatic storage, but has a different lifetime, which depends on what type of static it is.

Static storage has a lot of the same issues as regular global variables (in fact, globals are often static), so it is still best to avoid this type of storage where possible.

Ownership vs Aliasing

Ownership is not about how many people/things can point to an object. Having a std::unique_ptr pointing to something does not mean you’re not allowed or shouldn’t have other pointers or references to that same object. Having multiple ways of referring to a single object is called “aliasing”.

Ownership does mean that you should not have any other smart pointers pointing to the object, however. This is because smart pointers represent ownership of an object, and a std::unique_ptr is supposed to have exclusive ownership of the thing it points to.

Having a raw pointer or reference pointing to the thing owned by a std::unique_ptr does not change anything about ownership, and so is completely fine - as long as you don’t do anything ownership-related with the raw pointer or reference.

C++ types that model ownership

The above are in order of preference

C++ types that do not model ownership

Leaking memory with smart pointers in C++

Using smart pointers doesn’t mean you no longer have to think about ownership at all, it just makes sure that you’re thinking about certain aspects of ownership. It is still possible, for example, to leak memory with std::shared_ptrs:

struct Blah{
    std::shared_ptr<Blah> other;
};

void leak(){
    auto a = std::make_shared<Blah>();
    auto b = std::make_shared<Blah>();
    a->other = b;
    b->other = a;
} //The two Blah objects will still exist, despite a and b being cleaned up, because they both keep a reference to the other, holding the refernce counts above zero

It’s quite possible to do the same with std::unique_ptr, but it is a little bit harder to do without it being obvious that you’re playing with fire:

struct Blah{
    std::unique_ptr<Blah> other;
};

void leak(){
    auto a = std::make_unique<Blah>();
    auto b = std::make_unique<Blah>();
    a->other = std::move(b);
    a->other->other = std::move(a);
    //Really the only hint is the extra level of indirection, and the fact that if we did it like the shared_ptr example, without the extra indirection,  we'd get a crash every time for derefencing the now null `b`.
}

How to fix

Double delete with smart pointers in C++

It is also very possible to still have double delete issues even using smart pointers, but it’s a little harder to do accidentally using the standard smart pointers:

How To fix

What type to use

Function Parameters

Notes

Function return values

A note on return type deduction

auto will never deduce a reference (though it can deduce a pointer), and return type deduction is no exception. If you need to be able to deduce a reference type, use decltype(auto) instead. This is rarely what you want, though, and is here as a note to those interested in writing very generic code (templates / TMP)

Local variables

Class non-static member variables

Lambda captures

This is exactly the same as with a non-static data member, as that’s all lambdas are: syntactic sugar for defining and instantiating a class inline.

Just make sure you notice that when you’re passing a lambda to a callback or thread, the captures need to be available at the point of calling the callback or thread, so make sure you think about lifetimes and ownership and guarantee it with std::unique_ptr or std::shared_ptr.

Note also that copyability and moveability of captures affects the copyability and moveability of the lambda (as it would any class), so be sure to think about where you’re passing it to and how.

Sometimes it is necessary to mark a lambda as mutable e.g. []() mutable {}, as lambdas are immutable by default (unlike a class).

Static

Static lifetimes last until module unload, so do carefully consider if this has any implications on your objects and references.

Relying on the automatic cleanup of statics means the cleanup code will run at a non-deterministic time after main() has completed (or DLL has unloaded), so it can make debugging difficult.

Interfacing with C APIs

Older C++

For the case of non-owning optional semantics before C++17, use something like boost::optional if you are able, or fall back to using a raw pointer if you must. Make sure that you are careful with it.

For pre-C++11, using a 3rd party library, or, if you must, write your own, just be aware that move semantics are not available so move-only types are not possible (this is why std::auto_ptr is deprecated/removed). Falling back to raw pointers is not recommended at all. Even a typedef to put “owning” in the type name would be better than nothing.

Summary of type choice

Gotchas

Common dissenting remarks

Take-aways