The blog post introduces how to acheive composition in C++.

Abstract Classes
When we want to create classes that share common characteristics conceptually, but the concept itself is abstract, we can create an abstract class. A good example of this is when we want to use different shapes, all of which have the same function for determining the area.
int main () {
Shape *shapes = {
new Square(5),
new Rectangle(3,10);
new Circle(3);
};
for (int i = 0; i < 3; i++) {
cout << shapes[i] -> area() << endl;
}
for (int i = 0; i < 3; i++) {
delete shapes[i];
}
return 0;
}
All of the shapes—squares, rectangles, circles, and so on—have an area, but the concept of "shape" itself is not an object with a specified way of computing the area. In such scenarios, we can use an abstract class like the following:
class Shape {
public:
virtual float area () = 0;
};
class Square : public Shape {
public:
float side;
float area () {
return side * side;
};
Square (float side) : side(side) {};
}
...
When a class has a pure virtual function (a virtual function set to 0), the class becomes an abstract class, which cannot be instantiated as an object. If the classes that inherit from the abstract class do not override the pure virtual functions, an error occurs, ensuring runtime polymorphism.
Interfaces
In an abstract class, we are free to have attributes and methods other than pure virtual functions. However, many programmers prefer having only pure virtual functions in an abstract class for clarity. This is called an interface, and some other languages provide native support for interfaces. In C++, we can use an abstract class as an interface, like this:
class iShape {
public:
virtual double area () = 0;
virtual void print () = 0;
}
Interfaces are helpful when we want the abstract class to be used only for ensuring runtime polymorphism, while the reuse of attributes and methods can be achieved through composition.
Composition
Composition occurs when you use a class object as a class attribute. It makes sense to use it when classes have a "has-a" relationship, like in the example below:
class Date {
public:
int year;
int month;
int day;
void print (string format) {
if (format == "us") {
cout << month << "/" << day << "/" << year << endl;
}
else if (format == "uk" || format == "eu") {
cout << day << "/" << month << "/" << year << endl;
} else {
cout << year << "/" << month << "/" << day << endl;
}
};
Date (int y, int m, int d) : year(y), month(m), day(d) {};
};
class Person {
public:
string name;
Date birthday;
void print (string date_format) {
cout << "Name: " << name << endl;
cout << "Birthday: ";
birthday.print(date_format);
}
Person (string name, int y, int m, int d) : name(name), Date(y,m,d) {};
};
A person "has a" birthday (date), so it makes sense to include a Date
object as an attribute in the Person
class.
While composition is primarily used for "has-a" relationships, it can also be used for "is-a" relationships instead of inheritance.
class Bird {
public:
string name;
int age;
void chirp () {...};
};
class Crow {
public:
Bird bird;
void chirp () {
bird.chirp();
};
};
Inheritance vs Composition
Inheritance feels more natural when implementing an "is-a" relationship, but many prefer composition in various cases. The primary reason is that inheritance forces child classes to share all the attributes and methods of the parent class.
class Bird {
public:
string name;
int age;
virtual void chirp () {...};
virtual void fly () {...};
};
class Penguin : public Bird {
void chirp () {
...
};
void fly () {
return 0;
};
};
When using inheritance, we cannot choose to omit implementations of methods in the parent class.
In the example above, the implementation of fly
for Penguin
is awkward.
If we don't want the fly
method in Penguin, we would need to create an intermediate class like
FlightlessBird
or FlyableBird
and have the appropriate child classes inherit from it.
This can lead to tedious refactoring. Composition, on the other hand, allows the child class to
choose how it uses the attributes and methods of another class.
class Bird {
public:
string name;
int age;
void chirp () {...};
void fly () {...};
};
class Penguin {
Bird bird; // still has access to the name and age
stirng get_name () {
return bird.name;
};
stirng get_age () {
return bird.age;
};
void chirp () {
// we can decide whether to use default bird.chirp here
// or to implement new one
...
};
};
Although we might need to set up more getter and setter functions,
this approach allows Penguin
to decide which attributes and methods of Bird
to use in its implementation.
When we want to ensure runtime polymorphism while using composition, we can use interfaces.
class iBird {
public:
virtual void chirp () = 0;
};
class Crow : public iBird {
Bird bird;
void chirp () {
...
}
void fly () {
bird.fly();
}
};
class Penguin : public iBird {
Bird bird;
void chirp () {
bird.chirp();
}
};
This is why composition is generally preferred by many developers. However, there are cases where inheritance makes more sense, so it's essential to analyze which approach is more appropriate for each situation.
Resources
- CodeAesthetic. 2023. The Flaws of Inheritance. YouTube.
- Portfolio Courses. 2022. Abstract Classes And Pure Virtual Functions | C++ Tutorial. YouTube.