Composition and inheritance are two different ways that classes relate to each other. Inheritance establishes an "is a" relationship; a derived class IS A specialized version of the base class. Composition establishes a "has a" relationship; one class HAS A contained object from another class as a data member. While both these relationships utilize code reuse, they result in designs that behave quite differently when your requirements change.
Inheritance is the correct abstraction when the derived class IS ACTUALLY A type of the base class, and must be valid in any place where the base class is expected to be used. An example of this would be employees ARE PERSONs, so they should be substitutable in any context that expects a PERSON.
#include <iostream>
#include <string>
class Person
{
private:
std::string m_name;
public:
explicit Person(std::string name)
// Treat this object as movable instead of copying it
: m_name{ std::move(name) }
{
}
const std::string& name() const
{
return m_name;
}
};
class Employee : public Person
{
private:
int m_id{};
public:
Employee(std::string name, int id)
: Person{ std::move(name) }
, m_id{ id }
{
}
int id() const
{
return m_id;
}
};
int main()
{
Employee e{ "Alex", 42 };
std::cout << e.name() << ' ' << e.id() << '\n';
return 0;
}
The Employee class inherits from Person publicly. So, each Employee object will contain a Person subobject, and will be substitutable wherever a Person is expected. The reason call to e.name() works is because Employee inherits the functionality; construction first initializes the Person subobject, then the Employee subobject, and destruction works in the reverse order. The relationship is fixed and defined in the type system; an Employee IS A Person.
Composition is often used to show a different idea. Instead of a class telling a reader it IS A type of another class, it simply holds an object of the other class. The same employee information shown above can be represented by having Employee HAS A Person object as a member variable:
#include <iostream>
#include <string>
class Person
{
private:
std::string m_name;
public:
explicit Person(std::string name)
: m_name{ std::move(name) }
{
}
// Bind by reference without allowing modification through it
const std::string& name() const
{
return m_name;
}
};
class Employee
{
private:
Person m_person;
int m_id{};
public:
Employee(std::string name, int id)
: m_person{ std::move(name) }
, m_id{ id }
{
}
const std::string& name() const
{
return m_person.name();
}
int id() const
{
return m_id;
}
};
int main()
{
Employee e{ "Alex", 42 };
std::cout << e.name() << ' ' << e.id() << '\n';
return 0;
}
The Employee class now contains an object named, you guessed it, Person; it doesn't inherit any type, it just forwards the call to name on to the contained object. Now two classes are somewhat detached; an Employee IS NOT A Person, so it will not substitute a Person& in any functions where a Person& was expected. This can be a desired characteristic; changing Person will only affect Employee through the very narrow and controlled use of a member object, and the Person object can even be changed to something else with no impact to an inheritance hierarchy.
Composition can also make it easier to represent non permanent relations or relations of one type with multiples of other types; a department HAS MANY employees is a very natural one to implement with composition:
#include <iostream>
#include <string>
class Employee
{
private:
std::string m_name;
int m_id{};
public:
Employee(std::string name, int id)
: m_name{ std::move(name) }
, m_id{ id }
{
}
const std::string& name() const
{
return m_name;
}
};
class Department
{
private:
Employee m_staff[3];
int m_count{};
public:
void add(const Employee& e)
{
if (m_count < 3)
{
m_staff[m_count] = e;
// Increment the value before the expression uses it
++m_count;
}
}
void print() const
{
for (int i = 0; i < m_count; ++i)
std::cout << m_staff[i].name() << '\n';
}
};
int main()
{
Department d;
d.add(Employee{ "Alex", 1 });
d.add(Employee{ "Blake", 2 });
d.print();
return 0;
}
This Department class contains an array of Employee objects. It HAS A. The relation "is a" is just not used.
If polymorphic behavior is required, inheritance must be used. If it turns out that multiple kinds of employee must exist in memory and be worked on through a common Person interface, public inheritance will enable the necessary use of virtual functions and dynamically dispatched calls. Composition cannot do this alone.
The useful takeaway from all of this: when writing classes, favor composition when the class uses or holds another class, and reserve inheritance for genuine subtyping relationships defined in the type system, so that substitutability can be relied on, but remember that that relationship will often make the code harder to refactor later.
Exercise: Composition Versus InheritanceStarter code, hints and solutionOpenClose
Practice: Composition Versus Inheritance
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Bind a const Person& to e and print its name through that view. Identify the static and complete object types and explain how a composed Person member would differ.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <utility>
#include <iostream>
#include <string>
class Person
{
private:
std::string m_name;
public:
explicit Person(std::string name)
// Treat this object as movable instead of copying it
: m_name{ std::move(name) }
{
}
const std::string& name() const
{
return m_name;
}
};
class Employee : public Person
{
private:
int m_id{};
public:
Employee(std::string name, int id)
: Person{ std::move(name) }
, m_id{ id }
{
}
int id() const
{
return m_id;
}
};
int main()
{
Employee e{ "Alex", 42 };
std::cout << e.name() << ' ' << e.id() << '\n';
return 0;
}Hints
The example models Employee as a Person using public inheritance. m_id is additional employee state, while the name belongs to the Person base.
To sketch composition, imagine an Employee containing a Person member instead. Access would go through that member or a forwarding function rather than an inherited name() call.
Worked solution
A const Person& is bound to e and used to read its inherited name. The view has static type const Person&, while the complete object remains an Employee. Public inheritance permits that binding; a composed Person member would require explicit member access instead.
#include <utility>
#include <iostream>
#include <string>
class Person
{
private:
std::string m_name;
public:
explicit Person(std::string name)
// Treat this object as movable instead of copying it
: m_name{ std::move(name) }
{
}
const std::string& name() const
{
return m_name;
}
};
class Employee : public Person
{
private:
int m_id{};
public:
Employee(std::string name, int id)
: Person{ std::move(name) }
, m_id{ id }
{
}
int id() const
{
return m_id;
}
};
int main()
{
Employee e{ "Alex", 42 };
std::cout << e.name() << ' ' << e.id() << '\n';
const Person& person = e;
std::cout << "Base view: " << person.name() << '\n';
return 0;
}
Example output
Alex 42
Base view: Alex
0 comments
No comments yet. Be the first.