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 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 |
|---|---|
|
|
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 | |
|---|---|
|
|
| Copy constructor | Destructor |
|---|---|
|
|
| Settor | Gettor |
|---|---|
|
|
| Display (output) | |
|---|---|
|
|
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):
Student::Student(const Student& student)
{
set_login(student.login_); // String -> const char *?
set_age(student.age_);
set_year(student.year_);
set_GPA(student.GPA_);
}
Let's first rewrite it using a proper member initializer list:
Student::Student(const Student& student) : login_(student.login_),
age_(student.age_),
year_(student.year_),
GPA_(student.GPA_)
{
}
Is this better? What happens to our data-validation code?
Also, the get_login() method is also problematic:
const char *Student::get_login() const
{
return login_;
}
Both of these problems result in these errors: Student class
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.)
|
|
Let's look at a very simple program that demonstrates a very important concept.
| Code | Annotated Output |
|---|---|
|
[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:
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...
};
All of the implementation details are delegated to the aggregated objects (e.g. Engine, Dashboard, 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:
// 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;
}
Notice the use of the member initializer list in the copy constructor but not the non-default
constructor. What's the difference?
That leads to this:
| Code | Annotated Output |
|---|---|
|
[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 |
|---|---|
|
[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:
void Student::set_login(const char* login)
{
// login must be converted to a String object first.
login_ = login;
}
What's the solution?
| Declaration | Implementation |
|---|---|
void set_login(const String& 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:
|
|
const char *Student::get_login() const
{
// Conversion required (login is a String)
return login_;
}
If we want to prevent these "silent" conversions, we have to remove the conversion function that we implemented:
// Automatic (and silent) conversion from String to const char *
String::operator const char *() const
{
return string_;
}
String implementation
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 *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?
error: no matching function for call to `Student::Student()'
note: candidates are: Student::Student(const Student&)
note: Student::Student(const char*, int, int, float)
Larger example:
| 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.