Part C - Encapsulation
Member Operators
Identify the operators that can be overloaded for operands of compound type
Describe the syntax for defining member functions as operators on operands of compound type
Demonstrate unary and binary member operator overloads
"Programmers hate surprises: Overload operators only for good reason, and
preserve natural semantics; if that's difficult, you might be misusing
operator overloading" (Sutter, Alexandrescu, 2005)
Operations
| Unary
| Binary
| Summary
| Exercise
Operators are built into the core of C++. The core defines the
logic for operands of fundamental type as well as the logic for the
assignment between two object of the same compound type. To define an
operation for any other operand of compound type requires an overload of the
operator for that type. That is, for a client to be able to specify
an operation on an object of compound type, we have to overload the
operator for that type. Overloading an operator involves adding
to the class definition a member function that defines the logic of the
operation.
This chapter lists the operators that C++ lets us overload and describes
how to define a member function that implements an operator overload.
The chapter covers both unary and binary operations on the current object.
Unary operations involve an operator and one operand. Binary operations
involve an operator and two operands.
Operations
C++ identifies an overloaded operation by its symbol and the type(s) of its
operand(s). The signature of a member function that overloads
the operator for an operand of compound type includes the symbol
and the type of its right operand, if any. The left operand
is the current object.
Candidates for Overloading
C++ lets us overload the following operators (amongst others):
- post-fix and pre-fix (++ --)
- assignment (= += -= *= /=
%=)
- unary arithmetic (+ -)
- binary arithmetic (+ - * /
%)
- relational (== < > <= >=
!=)
- logical (&& || !)
C++ DOES NOT allow overloading of the following operators (amongst others):
- the scope resolution operator (::)
- the member selection operator (.)
- the member selection through pointer to member operator
(.*)
- the conditional operator (?:)
C++ DOES NOT let us define new operators.
Grouping Operators
Operators group according to the number of operands that they take:
- unary - post-fix increment/decrement, pre-fix increment/decrement, pre-fix plus, pre-fix minus
- binary - assignment, compound assignment, arithmetic, relational, logical
- ternary - conditional operator
Members and Helpers
We can overload operators in either of two ways, as:
- member operators - part of the definition of the compound type
- helper operators - outside the definition of the compound type
Typically, we define operators that change the state of their left operand
as member operators and operators that do not change the state of their
operands as helper operators. The next chapter covers helper operators.
Overloading a Member Operator
The header of a member function that overloads an operator
consists of:
- a return data type
- the keyword operator
- an operator symbol
- function parantheses
- the operand type, if binary
The return type identifies the type of the evaluated expression.
For example, to overload the = operator for a
Student as the left operand and the address of
an unmodifiable C-style string as the right operand, we may insert the
following prototype into the class definition:
class Student {
// ...
void operator=(const char*);
// ...
};
// ...
Student harry(975);
harry = "ABDA"; // calls the overloaded operator
|
The keyword-symbol combination (operator and
=) takes the place of the member function's identifier.
Signature
Every overloaded member operator has its own signature, which consists of:
- the operator keyword
- the operation symbol
- the type of its right operand, if any
- the const status of the
operation
The compiler binds the call to the member function with the signature
that matches the operator symbol, the operand type and the const
status.
Promotion or Coercion
If the compiler cannot find an exact match for an operation's signature,
the compiler will attempt a rather complicated selection process to find an
optimal fit, promoting or coercing the operand value into related types as
necessary.
Definition
To copy the string into the grade
instance variable, the member function's definition looks something like:
void Student::operator=(const char* g) {
strncpy(grade, g, M);
grade[M] = '\0'; // required for strncpy
}
|
Since the return type is void the operation
evaluates to nothing. For the operation to evaluate to a
copy of the Student as modified, we can
write:
Student Student::operator=(const char* g) {
strncpy(grade, g, M);
grade[M] = '\0';
return *this;
}
|
Unary Operations
A unary operation
consists of one operator and one operand.
The header for a unary member operator takes the
form
Type operator symbol()
where Type is the type of the
evaluated expression. operator
identifies an operation. symbol identifies the kind of operation.
The left operand is the current object.
The operator does not take any explicit
parameters (with one exception - see post-fix operators below).
Pre-Fix Operators
We overload the pre-fix increment/decrement operators to increment/decrement
the current object and return the updated value. The header for
a pre-fix operator takes the form
Type operator++() or Type operator--()
For example, let us overload the pre-fix increment operator for our
Student class so that a pre-fix expression
increases all of the Student's grades by one grade
letter, if possible:
// Pre-Fix Operators
// preFixOps.cpp
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
Student operator++();
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n, const char* g) {
*this = Student(n, "");
}
void Student::Student(int n, const char* g) {
// see Current Object chapter for validation logic
no = n;
strcpy(grade, g);
}
void Student::display() const {
cout << no << ' ' << grade << endl;
}
Student Student::operator++() {
for (int i = 0; grade[i] != '\0'; i++)
if (grade[i] == 'F') grade[i] = 'D';
else if (grade[i] != 'A') grade[i]--;
return *this;
}
int main () {
Student harry(975,"BCADB");
harry.display();
(++harry).display();
harry.display();
}
|
975 BCADB
975 ABACA
975 ABACA
|
Post-Fix Operators
We overload the post-fix operators to increment/decrement the current
object after returning its value. The header for
a post-fix operator takes the form
Type operator++(int) or Type operator--(int)
The int type in the header
distinguishes the two post-fix operators from their pre-fix
counterparts.
For example, let us overload the incrementing post-fix
operator for our Student class so that a post-fix
expression increases all of the Student's grades by one
grade letter, if possible:
// Post-Fix Operators
// postFixOps.cpp
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int);
Student(int, const char*);
void display() const;
Student operator++();
Student operator++(int);
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
void Student::Student(int n, const char* g) {
// see Current Object chapter for validation logic
no = n;
strcpy(grade, g);
}
void Student::display() const {
cout << no << ' ' << grade << endl;
}
Student Student::operator++() {
for (int i = 0; grade[i] != '\0'; i++)
if (grade[i] == 'F') grade[i] = 'D';
else if (grade[i] != 'A') grade[i]--;
return *this;
}
Student Student::operator++(int) {
Student s = *this; // save the original
++(*this); // call the pre-fix operator
return s; // return the original
}
int main () {
Student harry(975,"BCADB");
harry.display();
(harry++).display();
harry.display();
}
|
975 BCADB
975 BCADB
975 ABACA
|
Note how we avoid duplicating logic by calling the pre-fix operator from the
post-fix operator.
Compare the return values of the pre-fix and post-fix operators.
The post-fix operator returns a copy of the
current object as it was before any changes took effect.
The pre-fix operator returns a copy of the current object
after the changes have taken effect.
Binary Operations
A binary operation consists of one operator and two operands.
The header for a binary member operator takes the form
Type operator symbol (type identifier)
where Type is the type of the
evaluated expression. operator identifies
an operation. symbol
identifies the kind of operation. type is the right operand's type. identifier is the right operand's name.
The left operand is the current object. The operator
takes one explicit parameter, which refers to the right operand.
For example, let us overload the += operator for the char as the
right operand, to add a single grade to a Student object:
// Overloading Operators
// operators.cpp
#include <iostream>
using namespace std;
const int M = 13;
class Student {
int no;
char grade[M+1];
public:
Student();
Student(int, const char*);
void set(int, const char*);
void display() const;
Student operator++();
Student operator++(int);
Student& operator+=(char g);
};
Student::Student() {
no = 0;
grade[0] = '\0';
}
Student::Student(int n) {
*this = Student(n, "");
}
void Student::Student(int n, const char* g) {
// see Current Object chapter for validation logic
no = n;
strcpy(grade, g);
}
void Student::display() const {
cout << no << ' ' << grade << endl;
}
Student Student::operator++() {
for (int i = 0; grade[i] != '\0'; i++)
if (grade[i] == 'F') grade[i] = 'D';
else if (grade[i] != 'A') grade[i]--;
return *this;
}
Student Student::operator++(int) {
Student s = *this; // save the original
++(*this); // call the pre-fix operator
return s; // return the original
}
Student& Student::operator+=(char g) {
int i = strlen(grade);
if (i < M) {
// add validation logic here
grade[i++] = g;
grade[i] = '\0';
}
return *this;
}
int main () {
Student harry(975,"BCADB");
harry.display();
harry += 'B';
harry.display();
}
|
975 BCADB
975 BCADBB
|
Summary
- we may overload operators for operands of a compound type
- we cannot redefine operations on fundamental types or define new operators
- the keyword operator followed by a symbol identifies the operation associated with the symbol
- the left operand in an operation implemented as a member function is the current object
- ideal candidates for overloading as member operators are those that modify the left operand
- the int keyword in the signature for increment/decrement operator distinguishes the post-fix operation from the pre-fix operation
Exercise
|