Overview
This lab builds two C++ classes, Engine and Car, that show how composition lets one class reuse another by owning it rather than inheriting from it. Starting from a fully standalone Engine class, you will build a Car class that stores an Engine as a private member, initializes it through a member initializer list, and delegates its own start() to the Engine it owns. In the Inheritance lab, Car was declared class Car : public Vehicle and inherited everything a vehicle needed. This lab's Car has no base class at all. it reuses behaviour by holding an Engine as a value, the same way it holds an int or a std::string.
Learning Objectives
By the end of this lab you will be able to:
- Declare a class that owns another class as a private member instead of deriving from it
- Initialize a member object with no default constructor through the member initializer list
- Delegate a method call to a composed object rather than overriding or inheriting it
- Explain the difference between a has-a relationship and an is-a relationship
- Judge when composition is the better fit for a design and when inheritance still is
Concept
The Inheritance lab's Car : public Vehicle relationship is decided once, in the class declaration, and never changes for the lifetime of the program. Every Car is permanently a Vehicle, whether that is useful or not, and a change to Vehicle ripples into every class that derives from it, the fragile base class problem. Inheritance also struggles when two classes need to share behaviour but are not really the same kind of thing. a Car and a Motorbike both need an engine, but forcing them to share a common base class just to reuse engine logic pulls in fields like doors that make no sense for a motorbike.
Composition solves this by building a class out of other objects it owns instead of classes it derives from. A class declares another class as an ordinary private member, exactly like Car in this lab declares Engine engine; alongside its int wheels;. If the composed type has no default constructor, the owning class initializes it through the member initializer list, Car(...) : engine(engine) {}, before its own constructor body runs. The owning class then reuses the composed object's behaviour by calling its methods directly, engine.start(), a pattern called delegation, rather than by overriding or inheriting anything.
This is why composition is so often recommended over inheritance. the composed class stays completely decoupled (Engine has no idea Car exists), the relationship reuses behaviour across types that share no ancestry, and nothing about it forces a rigid hierarchy onto types that only partially resemble each other. Inheritance still earns its place when the relationship really is is-a and many types need to be treated uniformly through one shared interface, dispatched polymorphically through a base pointer, exactly the Shape* pattern from the Abstraction and Interfaces lab. Composition is the better default when a class simply needs another object's behaviour without taking on its full identity.
Tasks
Task 1 - The standalone Engine class
Open Engine.h and Engine.cpp and complete the Engine class. It knows nothing about Car and must work entirely on its own.
- In
Engine.h, declare three private members,displacementLitres(float),horsepower(int) andfuelType(std::string). - Declare a constructor taking the three matching parameters, a
start()method, and three getters,getDisplacementLitres(),getHorsepower()andgetFuelType(). - In
Engine.cpp, implement the constructor by assigning each parameter to its matching member usingthis->. - Implement
start()to print the engine's displacement, horsepower and fuel type tostd::cout, followed byhas started. - Implement the three getters to return their matching member.
Success condition: a small test in main(), Engine e(1.6f, 130, "Petrol"); e.start();, compiles and runs with no Car anywhere in the program, and prints a single line describing that engine.
#pragma once
#include <string>
// Engine is a complete, independent class. Nothing about it knows or cares
// that a Car is going to own one - it works just as well standing alone.
class Engine
{
private:
// TODO: declare three private members: displacementLitres (float),
// horsepower (int), fuelType (std::string)
public:
Engine(float displacementLitres, int horsepower, std::string fuelType);
// TODO: declare start() const, returning void
// TODO: declare getDisplacementLitres() const returning float,
// getHorsepower() const returning int, getFuelType() const returning
// std::string
};
#include "Engine.h"
#include <iostream>
Engine::Engine(float displacementLitres, int horsepower, std::string fuelType)
{
// TODO: assign each parameter to its matching member using this->
}
void Engine::start() const
{
// TODO: print "Engine (<displacementLitres>L, <horsepower>hp,
// <fuelType>) has started" followed by std::endl
}
float Engine::getDisplacementLitres() const
{
// TODO: return displacementLitres
}
int Engine::getHorsepower() const
{
// TODO: return horsepower
}
std::string Engine::getFuelType() const
{
// TODO: return fuelType
}
Task 2 - Car: composed of an Engine
Open Car.h and Car.cpp and complete the Car class. Unlike the Inheritance lab's Car, this one has no base class at all. it declares an Engine as a member the same way it declares any other private field.
- In
Car.h, declare six private members,wheelsanddoors(int),engine(Engine), andcolour,make,model(std::string). - Declare a constructor taking the matching six parameters, a
start()method, six getters (getEngine()returnsconst Engine&), and five setters (there is nosetEnginein this lab). - In
Car.cpp, implement the constructor. SinceEnginehas no default constructor, initializeenginethrough the member initializer list,Car::Car(...) : engine(engine), then assign the remaining five parameters to their matching members in the constructor body usingthis->. - Implement
start()to printThe car has started, then callengine.start().Car::start()does not print an engine message itself. it delegates that to theEngineit owns. - Implement the six getters, and the five setters, matching the behaviour of the Inheritance lab's
Vehicle.setWheelssilently ignores a negative value, andsetColourprintsYou have great tastebefore assigning if the given colour is"RED".
Success condition: Car.h and Car.cpp compile alongside Engine.h/Engine.cpp. Task 3 builds a full program that constructs Cars with different Engines to confirm the delegation works.
#pragma once
#include <string>
#include "Engine.h"
// Car has no base class at all - compare this declaration to the
// Inheritance lab's Car, which reads "class Car : public Vehicle". This
// Car reuses Engine's behaviour by owning one as a member ("has-a",
// composition) instead of inheriting it from a base class ("is-a",
// inheritance).
class Car
{
private:
// TODO: declare six private members: wheels (int), doors (int),
// engine (Engine), colour, make, model (std::string)
public:
Car(int wheels, int doors, Engine engine, std::string colour, std::string make, std::string model);
// TODO: declare start() const, returning void
// TODO: declare getWheels(), getDoors() (int), getEngine() (const
// Engine&), getColour(), getMake(), getModel() (std::string), all const
// TODO: declare setWheels(int), setDoors(int), setColour(std::string),
// setMake(std::string), setModel(std::string)
};
#include "Car.h"
#include <iostream>
Car::Car(int wheels, int doors, Engine engine, std::string colour, std::string make, std::string model)
// TODO: initialize engine through the member initializer list, since
// Engine has no default constructor for the compiler to fall back on
{
// TODO: assign the remaining five parameters to their matching
// members using this->
}
void Car::start() const
{
// TODO: print "The car has started", then call engine.start() -
// delegate the engine-starting message to the Engine this Car owns
}
int Car::getWheels() const
{
// TODO: return wheels
}
int Car::getDoors() const
{
// TODO: return doors
}
const Engine& Car::getEngine() const
{
// TODO: return engine
}
std::string Car::getColour() const
{
// TODO: return colour
}
std::string Car::getMake() const
{
// TODO: return make
}
std::string Car::getModel() const
{
// TODO: return model
}
void Car::setWheels(int wheels)
{
// TODO: ignore negative values, otherwise assign to the wheels member
}
void Car::setDoors(int doors)
{
// TODO: assign to the doors member
}
void Car::setColour(std::string colour)
{
// TODO: if colour is "RED", print "You have great taste" first, then
// assign it to the colour member either way
}
void Car::setMake(std::string make)
{
// TODO: assign to the make member
}
void Car::setModel(std::string model)
{
// TODO: assign to the model member
}
Task 3 - Building cars from independent engines
Open CompositionDemo.cpp and complete main() so it exercises Engine on its own, then two Cars each built from a different Engine.
- Construct a standalone
Engine,Engine standaloneEngine(1.6f, 130, "Petrol");, and callstart()on it directly, with noCarinvolved. - Print a separator line,
"---". - Construct
Engine hatchbackEngine(1.0f, 99, "Petrol");, then constructCar myCar(4, 2, hatchbackEngine, "blue", "Peugeot", "106");. CallmyCar.start(), then print its make, model andgetEngine().getHorsepower(). - Print another separator line, then construct a second car,
Car myTruck(6, 2, Engine(3.0f, 210, "Diesel"), "white", "Ford", "Transit");, passing a temporaryEnginestraight into the constructor. CallmyTruck.start()and print its make, model and horsepower the same way.
Success condition: running the program prints the standalone engine's line, then each car's own start message followed by its own engine's line, then its make, model and horsepower, with the two cars reporting completely different horsepower and fuel type because each one owns a separately built Engine. No class in this file shares a common base class with any other. every relationship here is a value one class happens to hold.
#include <iostream>
#include "Engine.h"
#include "Car.h"
int main()
{
// TODO: construct Engine standaloneEngine(1.6f, 130, "Petrol") and
// call start() on it directly, with no Car involved
// TODO: print "---"
// TODO: construct Engine hatchbackEngine(1.0f, 99, "Petrol"), then
// Car myCar(4, 2, hatchbackEngine, "blue", "Peugeot", "106"). Call
// myCar.start(), then print its make, model and
// getEngine().getHorsepower()
// TODO: print "---"
// TODO: construct Car myTruck(6, 2, Engine(3.0f, 210, "Diesel"),
// "white", "Ford", "Transit"), passing a temporary Engine straight
// into the constructor. Call myTruck.start(), then print its make,
// model and horsepower
}
Solution
Try the tasks above before revealing the solution.