Aggregation
Aggregation is a relationship between classes. It is also a form of code-reuse, and is therefore a very important concept.
Nested classes | Contained/Composed object |
---|---|
class List1 { public: // Nested class (List1::Node1) class Node1 { public: int data; Node1 *next; }; Node1 node_; // contained object }; |
// Global class class Node2 { public: int data; Node2 *next; }; class List2 { public: Node2 node_; // contained object }; |
class Student
{
public:
// Constructor (non-default)
Student(const char * login, int age, int year, float GPA);
// Explicit (not compiler-generated) copy constructor
Student(const Student& student);
// Explicit (not compiler-generated) assignment operator
Student& operator=(const Student& student);
// Destructor (not compiler-generated)
~Student();
// Mutators (settors) are not const
void set_login(const char* login);
void set_age(int age);
void set_year(int year);
void set_GPA(float GPA);
// Accessor (gettors) are const
int get_age() const;
int get_year() const;
float get_GPA() const;
const char *get_login() const;
// Nothing will be modified by display
void display() const;
private:
char *login_;
int age_;
int year_;
float GPA_;
// Called by copy constructor and assignment operator
void copy_data(const Student& rhs);
};
Student header file, Student.h
Notes:
That's the easy part:
Original version | New version |
---|---|
class Student { public: // Public stuff... private: char * login_; // Other private stuff... }; |
#include "String.h" class Student { public: // Public stuff... private: String login_; // Other private stuff... }; |
Of course, this is going to set off a bunch of compiler warnings/errors with the rest of the code since it currently assumes we're using pointers and not Strings.
Here are the parts of the code that reference login_: String.h
Constructor | |
---|---|
Student::Student(const char * login, int age, int year, float GPA) : login_(0) { set_login(login); set_age(age); set_year(year); set_GPA(GPA); } |
|
Copy constructor | Destructor |
---|---|
Student::Student(const Student& student) : login_(0) { set_login(student.login_); set_age(student.age_); set_year(student.year_); set_GPA(student.GPA_); } |
Student::~Student() { delete [] login_; } |
Settor | Gettor |
---|---|
void Student::set_login(const char* login) { // Delete "old" login delete [] login_; // Allocate new one int len = (int)std::strlen(login); login_ = new char[len + 1]; std::strcpy(login_, login); } |
const char *Student::get_login() const { return login_; } |
Display (output) | |
---|---|
void Student::display() const { using std::cout; using std::endl; cout << "login: " << login_ << endl; cout << " age: " << age_ << endl; cout << " year: " << year_ << endl; cout << " GPA: " << GPA_ << endl; } |
|
Setting the login is now trivial:
void Student::set_login(const char* login) { login_ = login; // Safe, but currently inefficient. // Calls String::operator= after conversion. // Why does this work (i.e. is allowed)? }
Current String implementation
However, there is something that is not quite right about calls to set_login (in the copy constructor):
Let's first rewrite it using a proper member initializer list:Student::Student(const Student& student) { set_login(student.login_); // String -> const char *? set_age(student.age_); set_year(student.year_); set_GPA(student.GPA_); }
Is this better? What happens to our data-validation code?Student::Student(const Student& student) : login_(student.login_), age_(student.age_), year_(student.year_), GPA_(student.GPA_) { }
Also, the get_login() method is also problematic:
Both of these problems result in these errors: Student classconst char *Student::get_login() const { return login_; }
Summary of the problems:Student.cpp: In copy constructor `Student::Student(const Student&)': Student.cpp:25: error: no matching function for call to `Student::set_login(const String&)' Student.h:19: note: candidates are: void Student::set_login(const char*) Student.cpp: In member function `Student& Student::operator=(const Student&)': Student.cpp:40: error: no matching function for call to `Student::set_login(const String&)' Student.h:19: note: candidates are: void Student::set_login(const char*) Student.cpp: In member function `void Student::copy_data(const Student&)': Student.cpp:51: error: no matching function for call to `Student::set_login(const String&)' Student.h:19: note: candidates are: void Student::set_login(const char*) Student.cpp: In member function `const char* Student::get_login() const': Student.cpp:140: error: cannot convert `const String' to `const char*' in return
We can actually solve both at the same time. I'm going to "cheat" and take the easy way out for now.
(We will fix this soon.)
class String { public: // Conversion constructor String(const char *str); // Other public stuff... // Conversion operator operator const char *() const; }; |
String::operator const char *() const { return string_; } |
Let's look at a very simple program that demonstrates a very important concept.
Code | Annotated Output |
---|---|
void f1() { Student john("jdoe", 20, 3, 3.10f); john.display(); } Output: login: jdoe age: 20 year: 3 GPA: 3.1 |
[String] Default constructor [String] Conversion constructor: jdoe [String] operator= = jdoe [String] Destructor: jdoe [Student] Student constructor for jdoe login: jdoe age: 20 year: 3 GPA: 3.1 [Student] Student destructor for jdoe [String] Destructor: jdoe |
Updated Student.h Student.cpp member initialization list
#include "String.h" class Student { public: // Public stuff... private: String login_; // Other private stuff... };
Probably the biggest advantage of using a user-defined String instead of a built-in pointer is that the Student class no longer requires you to implement the copy constructor, copy assignment operator, or the destructor. They are not needed because the String class takes care of all of that! That's why aggregation is so powerful. If you don't have any built-in types in your high-level class (e.g. Student), you don't need to implement those methods as the aggregate types take care of everything themselves.
Here's an example of a high-level class:
All of the implementation details are delegated to the aggregated objects (e.g. Engine, Dashboard, etc.)class Car { public: void StartEngine(); void ShutoffEngine(); void Accelerate(int amount); void ApplyBrakes(int amount); void TurnWheel(double degrees); void FillTank(); void WashWindshield(); void HonkHorn(); void ToggleHeadLights(bool onoff); // etc.... // Pointers private: private: Engine engine_; Engine *engine_; SteeringWheel wheel_; SteeringWheel *wheel_; Tire tires[4]; /* alternatively ==> */ Tire *tires[4]; Dashboard dash_; Dashboard *dash_; GasTank tank_; GasTank *tank_; Windshield wshield_; Windshield *wshield_; Horn horn_; Horn *horn_; HeadLight hlights[2]_; HeadLight *hlights[2]_; // etc... };
Notes:
Making the Student and String Classes More Efficient
The first step is easy. Simply use the member initializer list to construct the contained String object only once:
Notice the use of the member initializer list in the copy constructor but not the non-default constructor. What's the difference?// Non-default constructor Student::Student(const char * login, int age, int year, float GPA) : login_(login) { set_age(age); set_year(year); set_GPA(GPA); std::cout << "[Student] Student constructor for " << login_ << std::endl; } // Copy constructor Student::Student(const Student& student) : login_(student.login_), age_(student.age_), year_(student.year_), GPA_(student.GPA_) { std::cout << "[Student] Student copy constructor for " << login_ << std::endl; }
That leads to this:
Code | Annotated Output |
---|---|
void f1() { Student john("jdoe", 20, 3, 3.10f); john.display(); } Output: login: jdoe age: 20 year: 3 GPA: 3.1 |
[String] Conversion constructor: jdoe [Student] Student constructor for jdoe login: jdoe age: 20 year: 3 GPA: 3.1 [Student] Student destructor for jdoe [String] Destructor: jdoe |
Let's look at another simple operation:
Code | Annotated Output |
---|---|
void f2() { Student john("jdoe", 20, 3, 3.10f); Student jane("jsmith", 22, 4, 3.82f); std::cout << "=============================\n"; john = jane; std::cout << "=============================\n"; } |
[String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jsmith [Student] Student constructor for jsmith ============================= [String] Conversion constructor: jsmith [String] operator= jdoe = jsmith [String] Destructor: jsmith [Student] Student operator= for jsmith ============================= [Student] Student destructor for jsmith [String] Destructor: jsmith [Student] Student destructor for jsmith [String] Destructor: jsmith |
This is the problem:
What's the solution?void Student::set_login(const char* login) { // login must be converted to a String object first. login_ = login; }
Declaration | Implementation |
---|---|
void set_login(const String& login); |
void Student::set_login(const String& login) { // No conversion required. // login is already a String object. login_ = login; } |
Notes:[String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jsmith [Student] Student constructor for jsmith ============================= [String] operator= jdoe = jsmith [Student] Student operator= for jsmith ============================= [Student] Student destructor for jsmith [String] Destructor: jsmith [Student] Student destructor for jsmith [String] Destructor: jsmith
Explicit vs. Implicit Conversions
We still have another "convenience" occurring:
class String { public: // Other public stuff... // Conversion operator operator const char *() const; }; |
String::operator const char *() const { return string_; } |
If we want to prevent these "silent" conversions, we have to remove the conversion function that we implemented:const char *Student::get_login() const { // Conversion required (login is a String) return login_; }
String implementation// Automatic (and silent) conversion from String to const char * String::operator const char *() const { return string_; }
How will we convert a String to a const char * now?
Declaration | Implementation |
---|---|
// Convert a String to a const char * const char *c_str() const; |
const char *String::c_str() const { return string_; } |
const char *Student::get_login() const { return login_.c_str(); }
Now, in C++11, we can have explicit conversion operators:
Notes on containment:// Non-automatic conversion from String to const char * (in the String class) explicit operator const char *() const;
Arrays of Objects (Revisited)
A previous Student class:What's wrong with the code below?
Error messages:int a[4]; // Values of elements? Student s1[4]; // Values of elements?
Larger example:error: no matching function for call to `Student::Student()' note: candidates are: Student::Student(const Student&) note: Student::Student(const char*, int, int, float)
Code | Output |
---|---|
int main() { std::cout << "\n1 ======================\n"; Student s[] = { Student("jdoe", 20, 3, 3.10f), Student("jsmith", 22, 4, 3.60f), Student("foobar", 18, 2, 2.10f), Student("stimpy", 20, 4, 3.0f) }; std::cout << "\n2 ======================\n"; for (int i = 0; i < 4; i++) s[i].display(); Student *ps1[4]; // Values of elements? for (int i = 0; i < 4; i++) ps1[i] = &s[i]; std::cout << "\n3 ======================\n"; for (int i = 0; i < 4; i++) ps1[i]->display(); std::cout << "\n4 ======================\n"; Student *ps2[4]; // Values of elements? for (int i = 0; i < 4; i++) ps2[i] = new Student("jdoe", 20, 3, 3.10f); std::cout << "\n5 ======================\n"; for (int i = 0; i < 4; i++) ps2[i]->display(); std::cout << "\n6 ======================\n"; for (int i = 0; i < 4; i++) delete ps2[i]; std::cout << "\n7 ======================\n"; return 0; } |
1 ========================================== [String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jsmith [Student] Student constructor for jsmith [String] Conversion constructor: foobar [Student] Student constructor for foobar [String] Conversion constructor: stimpy [Student] Student constructor for stimpy 2 ========================================== login: jdoe age: 20 year: 3 GPA: 3.1 login: jsmith age: 22 year: 4 GPA: 3.6 login: foobar age: 18 year: 2 GPA: 2.1 login: stimpy age: 20 year: 4 GPA: 3 3 ========================================== login: jdoe age: 20 year: 3 GPA: 3.1 login: jsmith age: 22 year: 4 GPA: 3.6 login: foobar age: 18 year: 2 GPA: 2.1 login: stimpy age: 20 year: 4 GPA: 3 4 ========================================== [String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jdoe [Student] Student constructor for jdoe [String] Conversion constructor: jdoe [Student] Student constructor for jdoe 5 ========================================== login: jdoe age: 20 year: 3 GPA: 3.1 login: jdoe age: 20 year: 3 GPA: 3.1 login: jdoe age: 20 year: 3 GPA: 3.1 login: jdoe age: 20 year: 3 GPA: 3.1 6 ========================================== [Student] Student destructor for jdoe [String] Destructor: jdoe [Student] Student destructor for jdoe [String] Destructor: jdoe [Student] Student destructor for jdoe [String] Destructor: jdoe [Student] Student destructor for jdoe [String] Destructor: jdoe 7 ========================================== [Student] Student destructor for stimpy [String] Destructor: stimpy [Student] Student destructor for foobar [String] Destructor: foobar [Student] Student destructor for jsmith [String] Destructor: jsmith [Student] Student destructor for jdoe [String] Destructor: jdoe |
Self check: Make sure you can follow the code above and understand exactly why each line of output is generated. This will tell you if you understand these concepts or not.