Ownership and Software Design
Clearly communicating the intent of your code is one of the most powerful skills you can acquire in software engineering. It increases productivity, reduces stress, and lowers the entry barrier for new software developers. One aspect of a software application that is oftentimes not communicated clearly is ownership. Ownership, although simple in definition, is a powerful principle that not only solves common problems with memory management and multithreading, but also aids developers in creating well-designed software that is easy to understand—software that new developers can wrap their heads around quickly.
Most of what has been written about ownership focuses on its benefits when it comes to resource management. There is even a [popular programming language][rust-lang-ownership] built entirely upon the foundation of this principle. Interestingly, not as much is said about the advantages of ownership when it comes to software design. As such, I’d like to focus my thoughts on this specific benefit.
What is Ownership?
Ownership has a rather simple definition, but to understand it properly it helps to invert the way we think about variables and values in a program. We often think first about (1) creating variables and (2) assigning a value to them; however, ownership makes more sense if we think first about (1) the values in our system and (2) how they are related to variables.
With this inverted way of thinking, we can summarize the principle of ownership as follows: every value in a program has at least one variable that owns it.
When we say own, we mean that the variable is responsible for the management of the value during the course of its lifetime. When the owning variable goes out of scope or is otherwise removed, the value is discarded, and any allocated resources are freed (heap space, file descriptors, etc.). From a less technical perspective, clearly communicating the ownership of values in a program helps developers understand how to extend, build upon, and maintain the program.
Variables in a program fall into one of three categories when it comes to ownership: sole owner, shared owner, or borrower. To illustrate each category, we will use ownership as it applied to the C++ programming language.
Sole Owner
In sole ownership, a value is owned by one and only one variable. The only way to re-assign a value to another variable is through what is called a move. Doing so transfers the responsibility of managing the value to another variable. This is the strictest way to enforce and communicate ownership.
Consider the following function signature that deals with a class representing some real-estate property we can acquire from an agency:
struct Property { ... };
struct Agency {
Property* get_property();
};
The signature of get_property()
raises an important question: who owns the
return value? Is it the Agency
? Or is it the caller of get_property()
? We
could assume that since, in the real world, getting property means you become
the owner that the caller now owns the return value of get_property()
. But
assumptions like this are dangerous in programming, and that danger makes it
difficult to use the Agency
class properly. This may not be a problem for the
original creators of a software application because they already understand the
relationship between Agency
and the caller, but it introduces a significant
entry barrier when onboarding new developers.
Let’s rewrite the function signature of get_property()
with a special focus on
communicating ownership clearly:
struct Agency {
::std::unique_ptr<Property> get_property();
};
Thanks to the [std::unique_ptr
][unique-ptr], we know that once we call
get_property()
, we become the owners of the resulting value. This clearly
defines the relationship of values between Agency
and the caller.
Communicating sole ownership when creating new objects is even more important.
Let’s assume we modify Property
to have a constructor that allows it to have a
Building
:
Property(Building* pComputer);
We use pointers to take advantage of polymorphism, since there are many
different kinds of buildings we can put on our property. However, someone
looking at the function signature above encounters the same problem as before.
Who owns the Building
once Property
is constructed?
Let’s refactor it to communicate ownership again:
Property(::std::unique_ptr<Building> pComputer);
This function signature helps developers understand a lot more about the
responsibilities of the Property
class. It communicates clearly that
Property
owns a Building
, meaning the Property
class takes full
responsibility for managing the Building
value assigned to it. This makes
sense in the real-world, too. If you remove the property, you would expect
everything on it to be removed as well.
Shared Owner
Shared ownership allows a single value to be owned by multiple variables. The variables must use some method of coordination in order to determine who the last owner is, so that when the final owner goes out of scope the value is discarded properly.
Building on our real-estate example, let’s say two people go in to buy a piece of property:
struct Person {
Property* _property;
void purchase_property(Property* property);
}
Person alice;
Person bob;
auto pProperty = new Property{};
alice.purchase_property(pProperty);
bob.purchase_property(pProperty);
Based on our understanding thus far, we can recognize the potential difficulties
in understanding the relationships between classes using the code above. Who
really owns the property? In other words, who is responsible for the
Property
resource management? Technical details aside, these questions make it
difficult for someone reading the code to understand the relationships between
classes and values, consequentially making it difficult to use the Property
and Person
classes.
Since we want to communicate ownership, let’s rewrite this using
[std::shared_ptr
][shared-ptr]:
struct Person {
::std::shared_ptr<Property> _pProperty;
void purchase_property(::std::shared_ptr<Property> pProperty);
}
Person alice;
Person bob;
auto pProperty = ::std::make_shared<Property>();
alice.purchase_property(pProperty);
bob.purchase_property(pProperty);
Now we know exactly how the value of pProperty
is managed. std::shared_ptr
will keep track of all the owners and clean up resources when the last one is
removed, and it also communicates that both alice
and bob
share ownership
with the value of pProperty
.
Borrower
Borrowers only reference the value owned by another variable, which means that they cannot live longer than the value’s owner. This duration of life is known as a variable’s lifetime. Once the lifetime of a variable ends, any values it owns must be discarded properly.
In our example, let’s assume an appraiser comes to inspect the property. The
Appraiser
doesn’t own the property, so we can use a reference to communicate
that they never own the value of pProperty
. Using a reference also helps
communicate that the Appraiser
also can’t live longer than the Property
value they are inspecting (which makes sense in the real world, too).
struct Appraiser {
void inspect(const Property& property);
};
auto pProperty = ::std::make_unique<Property>();
Appraiser carol;
carol.inspect(*pProperty);
By using a reference here, we properly communicate the relationship between the
Appraiser
and Property
. We could have passed a reference to
std::unique_ptr
or std::shared_ptr
as well, but these communicate less
clearly the exact relationship (see the appendix below), and if one forgets the
ampersand (&
) indicating the reference, ownership would not be communicated
properly.
Note that C++17 introduces [
std::reference_wrapper
][reference-wrapper], which can be used to indicate the borrower relationship for functors and other objects that want to contain persistent references to other values rather than pointers.
Communicating Ownership Produces Well-designed Software
Ownership, although an often overlooked principle, is fundamental to well-written software. Communicating ownership properly helps us get the most out of agile software development, and although external documentation is important, using code as documentation is the fastest and most stress-free way of helping other developers understand the design of the system.
Appendix: C++ Ownership Reference
The following information is helpful when deciding how to communicate ownership
for a value of type T
.
Type | Description |
---|---|
std::unique_ptr<T> |
The variable is the sole owner. |
std::unique_ptr<T>& |
The borrower needs to modify the std::unique_ptr itself, not the value it points to. |
const std::unique_ptr<T>& |
The borrower needs access to information about the std::unique_ptr . |
std::shared_ptr<T> |
The variable is one of many owners (shared ownership). |
std::shared_ptr<T>& |
The borrower needs to modify the std::shared_ptr itself, not the value it points to. |
const std::shared_ptr<T>& |
The borrower needs access to information about the std::shared_ptr . |
T |
The owner is the sole owner of a new value. |
T& |
The borrower needs to modify the value. |
const T& |
The borrower needs information about the value, but doesn’t need to modify it. |
Table 1: Communicating ownership with types in C++
Additional information about some common practices for communicating ownership can be found in the [C++ Core Guidelines][c++-core-guidelines].
[rust-lang-ownership]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html [unique-ptr]: https://en.cppreference.com/w/cpp/memory/unique_ptr [shared-ptr]: https://en.cppreference.com/w/cpp/memory/shared_ptr [reference-wrapper]: https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper [c++-core-guidelines]: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-resource