Classes Part 4

Aggregation

Aggregation is a relationship between classes. It is also a form of code-reuse, and is therefore a very important concept. Recall the Student class from before:

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
Student implementation file, Student.cpp

Notes:

That's the easy part:

Original versionNew 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 constructorDestructor
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_;
}

SettorGettor
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;

}
Changes that need to be made:

  1. Constructors - remove the 0 initialization, it's not a pointer anymore.
  2. Destructor - remove the delete statement altogether. (It's not a pointer or dynamically allocated array)
  3. Gettor - needs to be changed to return the correct type.
  4. Settor - incompatible types
  5. Display - OK as is, there is already an overloaded operator<< for a String , thanks to our implementation.

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
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	
Summary of the problems:
  1. We need to overload the set_login method to accept a String as a parameter. Currently, it takes a const char * only.
  2. We need to convert the return value in get_login from a String to a const char * and return that.



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_;
}
Notes:

Let's look at a very simple program that demonstrates a very important concept.

CodeAnnotated 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
How many String objects were created? (String implementation)

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:
CodeAnnotated 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

Old 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
This trivial change can have a significant effect on the runtime properties of the program. My simple tests show that the efficient method is twice as fast as the inefficient way.


Let's look at another simple operation:

CodeAnnotated 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:

void Student::set_login(const char* login)
{
    // login must be converted to a String object first.
  login_ = login;
}
What's the solution?

DeclarationImplementation
void set_login(const String& login);
void Student::set_login(const String& login)
{
    // No conversion required.
    // login is already a String object.
  login_ = login;
}
Now, the output looks like this:

[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
Notes:

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_;
}
This is required to support this function in Student:
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?

DeclarationImplementation
// Convert a String to a const char *
const char *c_str() const;
const char *String::c_str() const
{
  return string_;
}
Now we can modify the function to call this explicitly instead of the implicit conversion function:

const char *Student::get_login() const
{
  return login_.c_str();
}

Now, in C++11, we can have explicit conversion operators:

  // Non-automatic conversion from String to const char * (in the String class)
explicit operator const char *() const;
Notes on containment:

Arrays of Objects (Revisited)

A previous Student class:

Student.h     Student.cpp

What's wrong with the code below?

int a[4];      // Values of elements?
Student s1[4]; // Values of elements?
Error messages:
 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:
CodeOutput
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.