Part C - Encapsulation
Helper Functions
Support a class definition with global function definitions
Describe the syntax for overloading operators that support a class
Grant a global function access to the private members of a class
"Avoid membership fees: Where possible, prefer making
functions nonmember non-friends" (Sutter, Alexandrescu, 2005)
Free
| Operators
| Friends
| Summary
| Exercise
In object-oriented programming, not all instructions that support a class need to be
included in the class definition. A well-encapsulated class can accept external
support in the form of global functions containing additional logic. We call these
functions helper functions. Helper functions access the objects of a class
solely through their parameters, all of which are explicit. A typical helper
function includes at least one parameter of the class type that it supports.
This chapter describes the definition of helper functions, including helper
operators, and discusses the granting of privileged access to
the private members of a class.
Free Helpers
A free or loosely coupled helper function is a function that obtains all of its information
from the public member functions of the class that it supports. A free helper function
does not require access to the private members of its class.
The coupling between a free helper function and its class is minimal, which
is an ideal design solution.
Comparison
Consider a helper function that compares two objects of the same class. The function
returns true if the objects have the same data values and
false if they differ.
Example
Let us add three queries (getStudentNo(), getNoGrades()
and getGrade()) to our Student class definition and
a helper function named areIdentical() as support. For
conciseness, let us assume that all grades are stored in static memory. We insert
the prototype for our helper function into the header file after the class definition:
// Student.h
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display() const;
int getStudentNo() const { return no; }
int getNoGrades() const { return ng; }
float getGrade(int i) const { return i < ng ? grade[i] : 0.0f; }
};
bool areIdentical(const Student&, const Student&);
|
The implementation file contains the definition of our helper function.
// Student.cpp
#include <iostream>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
*this = Student(n, nullptr, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
bool areIdentical(const Student& lhs, const Student& rhs) {
bool same = lhs.getStudentNo() == rhs.getStudentNo() &&
lhs.getNoGrades() == rhs.getNoGrades();
for (int i = 0; i < lhs.getNoGrades() && same; i++)
same = lhs.getGrade(i) == rhs.getGrade(i);
return same;
}
|
The following client code compares the two objects:
// Compare Objects
// compare.cpp
#include <iostream>
#include "Student.h"
using namespace std;
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), harry_(1234, gh, 3);
if (areIdentical(harry, harry_))
cout << "are identical" << endl;
else
cout << "are different" << endl;
}
|
are identical
|
The Cost of Upgrading Freedom
Free helper functions use public queries to access information that is otherwise
inaccessible. If we add a data member to the class, we may also need to add
a query to access its value. As we add data members, the class definition grows
with new queries. We call this growth class bloat.
One alternative to class bloat that admits upgradability is friendship (see below).
Helper Operators
Helper operators are global functions that overload operators.
Candidates for helper operators are operators that do not change
the values of their operands as shown in the table below.
Effect on Operand(s) |
Candidate |
Operands |
Operator |
Left Operand Changes |
Member |
0 |
++ -- - + ! & * |
1 |
= += -= *= /= %= |
Neither Operand Changes |
Helper |
2 |
+ - * / % == != >= <= > < << >> |
Identity Comparison
To improve readability, let us replace the
areIdentical() function defined above
with an overloaded == operator that
takes two Student operands.
The header file for the Student
class now contains:
// Student.h
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
void display() const;
int getStudentNo() const { return no; }
int getNoGrades() const { return ng; }
float getGrade(int i) const { return i < ng ? grade[i] : 0.0f; }
};
bool operator==(const Student&, const Student&);
|
The implementation file contains defines this helper operator:
bool operator==(const Student& lhs, const Student& rhs) {
bool same = lhs.getStudentNo() == rhs.getStudentNo() &&
lhs.getNoGrades() == rhs.getNoGrades();
for (int i = 0; i < lhs.getNoGrades() && same; i++)
same = lhs.getGrade(i) == rhs.getGrade(i);
return same;
}
|
The following client code compares the two objects:
// Compare Objects
// compare.cpp
#include <iostream>
#include "Student.h"
using namespace std;
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), harry_(1234, gh, 3);
if (harry == harry_)
cout << "are identical" << endl;
else
cout << "are different" << endl;
}
|
are identical
|
Addition
Consider an expression that adds a single grade to a Student
object and evaluates to a copy of the updated object. To implement this operation,
let us overload the + operator for a Student
object as the left operand and a float as the right operand.
As part of the class definition, we include the += operator
described in the preceding chapter on Member Operators.
The header file for the Student class now contains:
// Student.h
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
const Student& operator+=(float);
void display() const;
int getStudentNo() const { return no; }
int getNoGrades() const { return ng; }
float getGrade(int i) const { return i < ng ? grade[i] : 0.0f; }
};
bool operator==(const Student&, const Student&);
Student operator+(const Student&, float);
|
We maintain loose coupling by initializing a new Student
object to the left operand and calling the += member
operator on that object to add the single grade:
Student operator+(const Student& s, float grade) {
Student copy = s; // makes a copy
copy += grade; // calls the += operator on copy
return copy; // return updated copy
}
|
For symmetry, we overload the addition operator for identical operand types
but in reverse order. The complete header file contains:
// Student.h
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
const Student& operator+=(float);
void display() const;
int getStudentNo() const { return no; }
int getNoGrades() const { return ng; }
float getGrade(int i) const { return i < ng ? grade[i] : 0.0f; }
};
bool operator==(const Student&, const Student&);
Student operator+(const Student&, float);
Student operator+(float, const Student&);
|
Our implementation calls the first version with the arguments reversed:
// Student.cpp
#include <iostream>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
const Student& Student::operator+=(float g) {
if (no != 0 && ng < NG && g >= 0.f && g <= 100.f)
grade[ng++] = g;
return *this;
}
bool operator==(const Student& lhs, const Student& rhs) {
bool same = lhs.getStudentNo() == rhs.getStudentNo() &&
lhs.getNoGrades() == rhs.getNoGrades();
for (int i = 0; i < lhs.getNoGrades() && same; i++)
same = lhs.getGrade(i) == rhs.getGrade(i);
return same;
}
Student operator+(const Student& student, float grade) {
Student copy = student; // makes a copy
copy += grade; // calls the += operator on copy
return copy; // return updated copy
}
Student operator+(float grade, const Student& student) {
return student + grade; // calls operator+(const
// Student&, float)
}
|
The following client code produces the results shown on the right:
// Helper Operator
// helper-addition-operator.cpp
#include <iostream>
#include "Student.h"
using namespace std;
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3);
harry = harry + 63.7f;
harry.display();
}
|
1234:
89.40
67.80
45.50
63.70
|
Friendship
Friendship grants helper functions access to the private members of a class. By
granting friendship status, a class lets a helper function access to any of its
private members: data members or member functions. Friendship minimizes
class bloat.
To grant a helper function friendship status, we declare the function a friend
and place its declaration inside the class definition.
A friendship declaration takes the form
friend Type identifier(type [, type, ...]);
where Type is the return type of the
function and identifier is the function's
name.
For example:
// Student.h
const int NG = 20;
class Student {
int no;
float grade[NG];
int ng;
public:
Student();
Student(int);
Student(int, const float*, int);
const Student& operator+=(float);
void display() const;
friend bool operator==(const Student&, const Student&);
};
Student operator+(const Student&, float);
Student operator+(float, const Student&);
|
Our implementation looks like:
// Student.cpp
#include <iostream>
using namespace std;
#include "Student.h"
Student::Student() {
no = 0;
ng = 0;
}
Student::Student(int n) {
float g[] = {0.0f};
*this = Student(n, g, 0);
}
Student::Student(int sn, const float* g, int ng_) {
bool valid = sn > 0 && g != nullptr && ng_ >= 0;
if (valid)
for (int i = 0; i < ng_ && valid; i++)
valid = g[i] >= 0.0f && g[i] <= 100.0f;
if (valid) {
// accept the client's data
no = sn;
ng = ng_ < NG ? ng_ : NG;
for (int i = 0; i < ng; i++)
grade[i] = g[i];
} else {
*this = Student();
}
}
void Student::display() const {
if (no > 0) {
cout << no << ":\n";
cout.setf(ios::fixed);
cout.precision(2);
for (int i = 0; i < ng; i++) {
cout.width(6);
cout << grade[i] << endl;
}
cout.unsetf(ios::fixed);
cout.precision(6);
} else {
cout << "no data available" << endl;
}
}
const Student& Student::operator+=(float g) {
if (no != 0 && ng < NG && g >= 0.f && g <= 100.f)
grade[ng++] = g;
return *this;
}
Student operator+(const Student& student, float grade) {
Student copy = student; // makes a copy
copy += grade; // calls the += operator on copy
return copy; // return updated copy
}
Student operator+(float grade, const Student& student) {
return student + grade; // calls operator+(const
// Student&, float)
}
bool operator==(const Student& lhs, const Student& rhs) {
bool same = lhs.no == rhs.no && lhs.ng == rhs.ng;
for (int i = 0; i < lhs.ng && same; i++)
same = lhs.grade[i] == rhs.grade[i];
return same;
}
|
We have added the keyword friend to the
declaration within the class definition. We do not apply the keyword
to the function definition.
The following client code compares the two objects:
// Friends
// friends.cpp
#include <iostream>
#include "Student.h"
using namespace std;
int main () {
float gh[] = {89.4f, 67.8f, 45.5f};
Student harry(1234, gh, 3), harry_(1234, gh, 3);
harry_ += 63.7f;
if (harry == harry_)
cout << "are identical" << endl;
else
cout << "are different" << endl;
}
|
are different
|
The Cost of Friendship
A class definition that grants friendship to a helper function allows that function
to alter the values of its private data members. Granting friendship pierces
encapsulation.
As a rule, we grant friendship judiciously only to helper functions that require both
read and write access to the private data members. Where read-only access
is all that a helper function needs, using queries is probably more advisable.
Friendship is the strongest relationship that a class can grant
an external entity.
Friendly Classes
One class can grant another class access to its private members. A class
friendship declaration takes the form
friend class Identifier;
where Identifier is the name of the
class to which the host class grants friendship privileges.
For example, an Administrator
class needs access to all information held within each
Student object. To grant this access, we
simply include a class friendship declaration within the Student class definition
// Student.h
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
const Student& operator+=(char);
friend bool areIdentical(const Student&, const Student&);
friend class Administrator;
};
|
No Reciprocity, Transitivity or Exclusivity
Friendship is neither reciprocal, transitive nor exclusive. Just because one
class is a friend of another class does not mean that the latter is a
friend of the former. Just because a class is a friend of another
class and that other class is a friend of yet another class does not mean
that the latter class is a friend of either of them. A friend of
one class may be a friend of any other class.
Consider three classes: a Student,
an Administrator and an
Auditor.
- Let the
Auditor be a friend of the
Administrator and the
Administrator be a friend
of the Student
-
Just because the Auditor is a
friend of any Administrator and
the Administrator is a
friend of any Student, the Administrator is not necessarily a friend of
the Auditor and the Student is not necessarily a friend of
the Administrator (lack of reciprocity)
-
Just because the Auditor is a
friend of any Administrator and
the Administrator is a friend of
any Student, the Auditor is not necessarily a friend of
any Student (lack of transitivity)
Summary
- a helper function is a global function that supports a class
- a helper function refers to the class that it supports
through its explicit parameter(s)
- a helper operator is typically an operator that does not change the value
of its operands
- a friend function has direct access to the private members
of the class that granted the function friendship
- friendship is neither reciprocal, transitive, nor exclusive
- free helper functions reduce coupling at the cost of bloating a class
- friendly helper functions reduce bloating at the cost of piercing encapsulation
Exercises
|