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



Previous Reading  Previous: Classes and Resources Next: Helper Functions   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License