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 copy constructor
    Student(const Student& student);

      // Explicit assignment operator
    Student& operator=(const Student& student);

      // Destructor
    ~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_;

    void copy_data(const Student& rhs);
};
Notes:

That's the easy part:

#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 errors with the rest of the code since it currently assumes we're using pointers.

Here are the parts of the code that reference login_:

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 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.

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?
}

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_);
  set_age(student.age_);
  set_year(student.year_);
  set_GPA(student.GPA_);
}
Let's first rewrite it using a proper member initializer list: (There's still a problem, though)
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:
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:
      // 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. (We've removed the login_(0) from the member initializer list in the constructors, otherwise, the program will crash.)

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)

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. Why 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 has a significant effect on the runtime properties of the program.


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.