Composition builds complex objects from simple ones by making one class *contain* another rather than deriving from it. A car has an engine; it is not an engine. The container owns the contained object, forwards the calls it cares about, and is free to change its internals later. Composition is usually the better first choice in C++, and the reason is that it has no inheritance traps.
Containment is the simplest relationship
A member of a class type is containment. The outer object owns the inner one, constructs it, and destroys it.
#include
#include
class Engine {
public:
explicit Engine(int horsepower) : horsepower_(horsepower) {}
int horsepower() const {
return horsepower_;
}
std::string describe() const {
return std::to_string(horsepower_) + " hp engine";
}
private:
int horsepower_;
};
class Car {
public:
Car(const std::string& model, int horsepower)
: model_(model), engine_(horsepower) {}
std::string describe() const {
return model_ + " with a " + engine_.describe();
}
private:
std::string model_;
Engine engine_;
};
int main() {
Car car("Falcon", 240);
std::cout << car.describe() << '\n';
return 0;
}The output shows the outer object combining its own data with the inner object's:
Falcon with a 240 hp engineThe Car constructor takes an int rather than an Engine, so the caller never has to build the engine separately. That is a design choice worth making deliberately: it keeps the parts of the API that the caller does not care about out of the way.
Forwarding and accessors
The contained object is private by default, so the outer class decides what to expose. Sometimes that means forwarding a method straight through.
#include
#include
class Engine {
public:
explicit Engine(int horsepower) : horsepower_(horsepower) {}
int horsepower() const { return horsepower_; }
void boost(int extra) { horsepower_ += extra; }
private:
int horsepower_;
};
class Car {
public:
Car(const std::string& model, int horsepower)
: model_(model), engine_(horsepower) {}
const std::string& model() const { return model_; }
int horsepower() const { return engine_.horsepower(); }
void boost(int extra) { engine_.boost(extra); }
private:
std::string model_;
Engine engine_;
};
int main() {
Car car("Falcon", 240);
std::cout << car.model() << ": " << car.horsepower() << " hp\n";
car.boost(20);
std::cout << "after boost: " << car.horsepower() << " hp\n";
return 0;
}The output shows the forwarded call reaching the engine:
Falcon: 240 hp
after boost: 260 hpComposition over inheritance
The classic mistake is modelling "a car is a kind of vehicle" with inheritance, and then discovering that a car is also "a kind of thing with wheels" and "a kind of thing that can be driven". With inheritance, only one of those can be the base class.
Composition has no such problem. A car can *contain* an engine, a gearbox, a chassis and a driver, and each of those can be reused elsewhere.
#include
#include
class Engine {
public:
explicit Engine(int horsepower) : horsepower_(horsepower) {}
int horsepower() const { return horsepower_; }
private:
int horsepower_;
};
class ElectricMotor {
public:
explicit ElectricMotor(int kilowatts) : kilowatts_(kilowatts) {}
int power_equivalent() const { return kilowatts_ * 2; }
private:
int kilowatts_;
};
class Vehicle {
public:
explicit Vehicle(std::string model) : model_(std::move(model)) {}
const std::string& model() const { return model_; }
private:
std::string model_;
};
class PetrolCar : public Vehicle {
public:
PetrolCar(std::string model, int horsepower)
: Vehicle(std::move(model)), engine_(horsepower) {}
int output() const { return engine_.horsepower(); }
private:
Engine engine_;
};
class ElectricCar : public Vehicle {
public:
ElectricCar(std::string model, int kilowatts)
: Vehicle(std::move(model)), motor_(kilowatts) {}
int output() const { return motor_.power_equivalent(); }
private:
ElectricMotor motor_;
};
int main() {
PetrolCar petrol("Falcon", 240);
ElectricCar electric("Spark", 120);
std::cout << petrol.model() << ": " << petrol.output() << " hp\n";
std::cout << electric.model() << ": " << electric.output() << " hp equivalent\n";
return 0;
}The output shows the two vehicles sharing the base but differing in the contained part:
Falcon: 240 hp
Spark: 240 hp equivalentThe fragile base class problem is the reason to prefer this: if the Vehicle base class later changes how it stores the model, both derived classes are affected. With composition, the shared part is a contained object that nobody inherits from.
Composition and ownership
The question "who owns the inner object?" decides the shape of the relationship.
- By value — the outer object owns the inner one. The inner object is created with the outer one and destroyed with it. This is the simplest and usually the right choice.
- By unique pointer — the outer object owns it but can transfer ownership. Use it when the inner object is optional or created later.
- By raw pointer — almost never the right choice. Somebody has to remember to delete it.
- By reference — the outer object does not own the inner one and must not destroy it. Use it when the inner object outlives the outer one and is shared.
Composition in a larger design
Real objects are composed of several parts, each responsible for one thing. The outer class coordinates them.
#include
#include
#include
class Sensor {
public:
Sensor(const std::string& name, int reading) : name_(name), reading_(reading) {}
const std::string& name() const { return name_; }
int reading() const { return reading_; }
private:
std::string name_;
int reading_;
};
class DiagnosticPanel {
public:
void add_sensor(const Sensor& sensor) {
sensors_.push_back(sensor);
}
int highest_reading() const {
int best = 0;
for (const Sensor& sensor : sensors_) {
if (sensor.reading() > best) {
best = sensor.reading();
}
}
return best;
}
void report() const {
std::cout << "sensors: " << sensors_.size() << '\n';
for (const Sensor& sensor : sensors_) {
std::cout << " " << sensor.name() << ": " << sensor.reading() << '\n';
}
}
private:
std::vector sensors_;
};
int main() {
DiagnosticPanel panel;
panel.add_sensor(Sensor{"oil pressure", 42});
panel.add_sensor(Sensor{"coolant", 91});
panel.add_sensor(Sensor{"battery", 12});
panel.report();
std::cout << "highest: " << panel.highest_reading() << '\n';
return 0;
}The output shows the panel coordinating its sensors:
sensors: 3
oil pressure: 42
coolant: 91
battery: 12
highest: 91Quick reference
- Containment means the outer object owns the inner one and controls its lifetime.
- Construct the inner object through the outer constructor so callers do not have to.
- Forward only the methods the outside world needs; everything else stays private.
- Prefer composition over inheritance when there is no genuine is-a relationship.
- The fragile base class problem is the classic reason to choose composition.
- Own by value, by
std::unique_ptrfor optional parts, by reference for borrowed parts.
Try it yourself: build a [[Vehicle]] class that contains an [[Engine]] and a [[FuelTank]], and a [[Car]] class that contains a [[Vehicle]] and adds driver assistance features. Then write a [[ElectricCar]] that contains the same [[Vehicle]] but a different power unit. Print a summary of each and check that the shared behaviour lives in one place.