Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Building Complex Objects from Simple Parts
Create objects that own and manage the lifetime of their parts.
Object composition
In real-life, complex objects are often built from smaller, simpler objects. For example, a computer is built using a CPU, motherboard, memory, storage, and other components. A bicycle is built from a frame, wheels, handlebars, pedals, and a chain. Even you are built from smaller parts: you have a brain, lungs, legs, arms, and so on. This process of building complex objects from simpler ones is called object composition.
Broadly speaking, object composition models a "has-a" relationship between two objects. A computer "has-a" CPU. Your bicycle "has-a" frame. You "have-a" brain. The complex object is sometimes called the whole, or the parent. The simpler object is often called the part, child, or component.
In C++, you've already seen that structs and classes can have data members of various types (such as fundamental types or other classes). When we build classes with data members, we're essentially constructing a complex object from simpler parts, which is object composition. For this reason, structs and classes are sometimes referred to as composite types.
Object Composition is useful in a C++ context because it allows us to create complex classes by combining simpler, more easily manageable parts. This reduces complexity and allows us to write code faster and with fewer errors because we can reuse code that has already been written, tested, and verified as working.
Types of object composition
There are two basic subtypes of object composition: composition and aggregation. We'll examine composition in this lesson, and aggregation in the next.
A note on terminology: the term "composition" is often used to refer to both composition and aggregation, not just to the composition subtype. In this tutorial, we'll use the term "object composition" when we're referring to both, and "composition" when we're referring specifically to the composition subtype.
Composition
To qualify as a composition, an object and a part must have the following relationship:
- The part (member) is part of the object (class)
- The part (member) can only belong to one object (class) at a time
- The part (member) has its existence managed by the object (class)
- The part (member) does not know about the existence of the object (class)
A good real-life example of a composition is the relationship between a computer's CPU and the computer itself. Let's examine these in more detail.
Composition relationships are part-whole relationships where the part must constitute part of the whole object. For example, a CPU is part of a computer. The part in a composition can only be part of one object at a time. A CPU that is part of one computer cannot be part of another computer at the same time.
In a composition relationship, the object is responsible for the existence of the parts. Most often, this means the part is created when the object is created, and destroyed when the object is destroyed. But more broadly, it means the object manages the part's lifetime in such a way that the user of the object does not need to get involved. For example, when a computer is assembled, the CPU is installed. When a computer is disassembled, the CPU is removed. Because of this, composition is sometimes called a "death relationship".
And finally, the part doesn't know about the existence of the whole. Your CPU operates blissfully unaware that it's part of a larger computer system. We call this a unidirectional relationship, because the computer knows about the CPU, but not the other way around.
Note that composition has nothing to say about the transferability of parts. A CPU can be removed from one computer and installed in another. However, even after being transferred, it still meets the requirements for a composition (the CPU is now owned by the recipient computer, and can only be part of the recipient computer unless transferred again).
A simple integer class is a great example of a composition:
class Coordinate
{
private:
int m_x;
int m_y;
public:
Coordinate(int x=0, int y=0)
: m_x{x}, m_y{y}
{
}
};
This class has two data members: an x-coordinate and a y-coordinate. The coordinates are part of the Coordinate (contained within it). They cannot belong to more than one Coordinate at a time. The coordinates don't know they are part of a Coordinate; they just hold integers. When a Coordinate instance is created, the coordinates are created. When the Coordinate instance is destroyed, the coordinates are destroyed as well.
While object composition models has-a type relationships (a computer has-a CPU, a coordinate has-a y-value), we can be more precise and say that composition models "part-of" relationships (a CPU is part-of a computer, a y-value is part of a coordinate). Composition is often used to model physical relationships, where one object is physically contained inside another.
The parts of an object composition can be singular or multiplicative -- for example, a CPU is a singular part of the computer, but a computer contains multiple memory modules (which could be modeled as an array).
Implementing compositions
Compositions are one of the easiest relationship types to implement in C++. They are typically created as structs or classes with normal data members. Because these data members exist directly as part of the struct/class, their lifetimes are bound to that of the class instance itself.
Compositions that need to do dynamic allocation or deallocation may be implemented using pointer data members. In this case, the composition class should be responsible for doing all necessary memory management itself (not the user of the class).
In general, if you can design a class using composition, you should design a class using composition. Classes designed using composition are straightforward, flexible, and robust (in that they clean up after themselves nicely).
More examples
Many games and simulations have entities or objects that move around a board, map, or screen. One thing that all of these entities/objects have in common is that they all have a location. In this example, we're going to create a spaceship class that uses a coordinate class to hold the spaceship's location.
First, let's design the coordinate class. Our spaceship is going to operate in a 2D universe, so our coordinate class will have 2 dimensions, X and Y. We'll assume the universe is made up of discrete squares, so these dimensions will always be integers.
Coordinate.h:
#pragma once
#include <iostream>
class Coordinate
{
private:
int m_x;
int m_y;
public:
// Default constructor
Coordinate()
: m_x{0}, m_y{0}
{
}
// Specific constructor
Coordinate(int x, int y)
: m_x{x}, m_y{y}
{
}
// Overloaded output operator
friend std::ostream& operator<<(std::ostream& out, const Coordinate& coord)
{
out << '(' << coord.m_x << ", " << coord.m_y << ')';
return out;
}
// Access functions
void setCoordinate(int x, int y)
{
m_x = x;
m_y = y;
}
};
Note that because we've implemented all of our functions in the header file (for the sake of keeping the example concise), there is no Coordinate.cpp.
This Coordinate class is a composition of its parts: location values x and y are part-of Coordinate, and their lifespan is tied to that of a given Coordinate instance.
Now let's design our Spaceship. Our Spaceship is going to have a few properties: an identifier, which will be a string, and a location, which will be our Coordinate class.
Spaceship.h:
#pragma once
#include <iostream>
#include <string>
#include <string_view>
#include "Coordinate.h"
class Spaceship
{
private:
std::string m_id;
Coordinate m_position;
public:
Spaceship(std::string_view id, const Coordinate& position)
: m_id{id}, m_position{position}
{
}
friend std::ostream& operator<<(std::ostream& out, const Spaceship& ship)
{
out << ship.m_id << " is at " << ship.m_position;
return out;
}
void moveTo(int x, int y)
{
m_position.setCoordinate(x, y);
}
};
This Spaceship is also a composition of its parts. The spaceship's identifier and position have one parent, and their lifetime is tied to that of the Spaceship they are part of.
And finally, main.cpp:
#include "Spaceship.h"
#include "Coordinate.h"
#include <iostream>
#include <string>
int main()
{
std::cout << "Enter an ID for your spaceship: ";
std::string id;
std::cin >> id;
Spaceship ship{id, {10, 15}};
while (true)
{
// print the spaceship's ID and location
std::cout << ship << '\n';
std::cout << "Enter new X coordinate for spaceship (-1 to quit): ";
int x{0};
std::cin >> x;
if (x == -1)
break;
std::cout << "Enter new Y coordinate for spaceship (-1 to quit): ";
int y{0};
std::cin >> y;
if (y == -1)
break;
ship.moveTo(x, y);
}
return 0;
}
Here's a transcript of this code being run:
Enter an ID for your spaceship: Falcon
Falcon is at (10, 15)
Enter new X coordinate for spaceship (-1 to quit): 20
Enter new Y coordinate for spaceship (-1 to quit): 25
Falcon is at (20, 25)
Enter new X coordinate for spaceship (-1 to quit): 5
Enter new Y coordinate for spaceship (-1 to quit): 8
Falcon is at (5, 8)
Enter new X coordinate for spaceship (-1 to quit): -1
Variants on the composition theme
Although most compositions directly create their parts when the composition is created and directly destroy their parts when the composition is destroyed, there are some variations of composition that bend these rules a bit.
For example:
- A composition may defer creation of some parts until they're needed. For example, a string class may not create a dynamic array of characters until the user assigns the string some data to hold.
- A composition may opt to use a part that has been given to it as input rather than create the part itself.
- A composition may delegate destruction of its parts to some other object (e.g., to a garbage collection routine).
The key point here is that the composition should manage its parts without the user of the composition needing to manage anything.
Composition and class members
One question that new programmers often ask when it comes to object composition is, "When should I use a class member instead of direct implementation of a feature?" For example, instead of using the Coordinate class to implement the Spaceship's location, we could have instead just added 2 integers to the Spaceship class and written code in the Spaceship class to handle the positioning. However, making Coordinate its own class (and a member of Spaceship) has a number of benefits:
- Each individual class can be kept relatively simple and straightforward, focused on performing one task well. This makes those classes easier to write and much easier to understand, as they are more focused. For example, Coordinate only worries about coordinate-related stuff, which helps keep it simple.
- Each class can be self-contained, which makes them reusable. For example, we could reuse our Coordinate class in a completely different application. Or if our spaceship ever needed another coordinate (for example, a destination it was trying to reach), we can simply add another Coordinate member variable.
- The outer class can have the class members do most of the hard work, and instead focus on coordinating the data flow between the members. This helps lower the overall complexity of the outer class, because it can delegate tasks to its members, who already know how to do those tasks. For example, when we move our Spaceship, it delegates that task to the Coordinate class, which already understands how to set a coordinate. Thus, the Spaceship class does not have to worry about how such things would be implemented.
Each class should be built to accomplish a single task: either the storage and manipulation of data (e.g., Coordinate, std::string), OR the coordination of its members (e.g., Spaceship). Ideally not both.
In this case of our example, it makes sense that Spaceship shouldn't have to worry about how Coordinates are implemented, or how the identifier is being stored. Spaceship's job isn't to know those intimate details. Spaceship's job is to worry about how to coordinate the data flow and ensure that each of the class members knows what it is supposed to do. It's up to the individual classes to worry about how they will do it.
Summary
Object composition: The process of building complex objects from simpler component objects. Models "has-a" or "part-of" relationships. Allows creating sophisticated functionality by combining simpler, reusable parts.
Composition definition: A relationship where the part (member) is part of the whole (class), can only belong to one object at a time, has its existence managed by the object, and doesn't know about the existence of the object (unidirectional).
Composition characteristics: The whole object is responsible for creating and destroying its parts. When the composition is destroyed, its parts are destroyed as well. This is sometimes called a "death relationship."
Implementation: Typically implemented using normal member variables. For dynamic allocation, the composition class should handle all memory management itself. The member's lifetime is bound to the class instance.
Transferability: Composition says nothing about whether parts can be transferred between objects. A part can be moved to a different owner while still maintaining a composition relationship with its new owner.
Benefits of composition: Keeps classes simple and focused on one task. Makes classes reusable in different contexts. Reduces complexity by delegating tasks to members who know how to handle them. Enables building on tested, working code.
Composition variants: A composition may defer creation of some parts until needed, accept parts as input rather than creating them, or delegate destruction to external systems. The key is that the composition manages its parts without user involvement.
Parts as singular or multiplicative: The parts can be single objects (one CPU per computer) or multiple objects (multiple memory modules per computer, often modeled as arrays).
If you can design a class using composition, you should design a class using composition. Composition creates straightforward, flexible, and robust classes that clean up after themselves.
Building Complex Objects from Simple Parts - Quiz
Test your understanding of the lesson.
Practice Exercises
Composition Relationship
Implement a composition relationship where a whole owns its parts. Practice creating classes where component objects cannot exist independently of the container.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!