Part C - Encapsulation

Custom I/O Operators

Overload the extraction and insertion operators as helper operators

"You don't need to modify istream or ostream to add new << and >> operators" (Stroustrup, 1997)

iostream | Design | Helper Operators | String Class | Summary | Exercises


The insertion and extraction operators are overloaded for the fundamental types in the iostream library.  Just like with the other operators of the core language, we can overload these two binary operators as helpers for right operands of compound type.  Overloading both extends their functionality to the compound type. 

This chapter describes how to overload the insertion and extraction operators for a compound type and how to implement cascading on the left operand.  From here onwards, we adopt full scope resolution syntax for all identifiers that belong to a namespace.  This chapter concludes by showing how to use the standard library's string class to determine the memory required to store character data from an input stream. 


iostream Implementations

The two operators for inserting values into an output stream and extracting from an input stream are:

  • << (insert into an output stream)
  • >> (extract from an input stream)

The iostream library overloads the insertion operator for objects of std::ostream compound type as the left operand and all of the fundamental types as right operands.  The library also defines the standard output objects in the standard namespace.  A client program accesses these standard objects by including the library's header file and applying the scope resolution operator:

 #include <iostream>

 int main() {
     std::cout << "Hello World" << std::endl; 
 }



 Hello World 

Similarly, the iostream library overloads the extraction operator for an object of std::istream compound type as the left operand and all of the fundamental types as right operands.  The library also defines the standard input object in the standard namespace.  A client program can access this standard object by including the library's header file and applying the scope resolution operator:

 #include <iostream>

 int main() {
     int x;

     std::cout << "Enter an integer : ";
     std::cin >> x;
     std::cout << "You entered " << x << std::endl; 
 }





 Enter an integer : 3 

 You entered 3

Once we overload the two operators for our Student class, a client will be able to use them as shown on the left:

 #include <iostream>
 #include "Student.h"

 int main() {
     Student harry;

     std::cin >> harry;
     std::cout << harry << std::endl;
 }





 Enter number : 1234
 Enter grades : ABACA
 1234 ABACA


Design Considerations

Three issues are important in overloading the insertion and extraction operators for compound types:

  • flexibility in the selection of output objects
  • scope resolution on classes and objects defined in the std namespace
  • enabling the same cascading that is available for fundamental types

Output Objects

The iostream library defines three standard output objects.  To enable selection of the output object by the client, we upgrade our display() member function to receive a reference to any object of std::ostream type:

 // Student.h

 #include <iostream> // for std::ostream 

 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int);
     Student(int, const char*);
     void display(std::ostream& os)
      const;
     Student& operator+=(char);
     int number() const { return no; } 
     const char* marks() const {
      return grade; }
 };
 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 // ...

 void Student::display(std::ostream& 
  os) const {
     os << no << ' ' << grade;
 }

 // ...


The client can now choose the output object (cout, cerr, or clog). 

Scope Resolution

The scope resolution syntax on the parameter to the display() function avoids exposing any other names defined in the std namespace.  Full scope resolution syntax only exposes the names that we actually use. 

This convention is considered superior design especially in the construction of header files.  Exposing all of the names in a namspace, including unused names, may lead to an unnecessary conflict when a new name is introduced. 

For example, in passing a reference to an object of the std::ostream class, which is defined in the std namespace, the preferred method of coding for header files is shown on the right:

 // Student.h

 #include <iostream>
 using namespace std; // POOR DESIGN
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int);
     Student(int, const char*);
     void display(ostream& os) const;
     Student& operator+=(char);
     int number() const { return no; } 
     const char* marks() const {
      return grade; }
 };
 // Student.h

 #include <iostream>  // GOOD DESIGN
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int);
     Student(int, const char*);
     void display(std::ostream& os)
      const; 
     Student& operator+=(char);
     int number() const { return no; } 
     const char* marks() const {
      return grade; }
 };

Cascading

Cascading support enables concatenation of operations into a compound expression where the leftmost operand serves as the left operand for every operation in the compound expression. 

For example, the cascaded expression

 std::cout << x << y << z << std::endl;

expands to two simpler sub-expressions executed in the following order:

 std::cout << x;
 std::cout << y << z << std::endl;

The cascaded sub-expression

 std::cout << y << z << std::endl;

expands to two simpler sub-expressions executed in the following order:

 std::cout << y;
 std::cout << z << std::endl;

Finally, the cascaded sub-expression

 std::cout << z << std::endl;

expands into two simpler sub-expressions executed in the following order:

 std::cout << z;
 std::cout << std::endl;

To enable cascading, we only need to return a modifiable reference to the left operand. 

Returning A Modifiable Reference

Returning a modifiable reference from a function enables the client to use the return value as the left operand for the operator on its right.  The call to an operator that returns a modifiable reference takes the following form after returning from the function:

 return value   next operator   next right operand
 

The next right operand may be a compound expression with more operators as shown in the cascading example above.


Two Helper Operators

The prototypes for overloaed insertion and extraction operators on a compound type take the form  

 std::istream& operator>>(std::istream&, Type&);
 std::ostream& operator<<(std::ostream&, const Type&);

where Type is the name of the compound type.  The modifiable reference in the extraction operator allows changes to the contents of the right operand. 

The header file for our Student class upgraded for these overloads is:

 // Student.h

 #include <iostream> // for std::ostream, std::istream
 const int M = 13;

 class Student {
     int no;
     char grade[M+1];
 public:
     Student();
     Student(int);
     Student(int, const char*);
     void display(std::ostream& os) const;
     Student& operator+=(char);
     bool empty() const { return no == 0; }
     int number() const { return no; }
     const char* marks() const { return grade; }
 };

 std::istream& operator>>(std::istream& is, Student& s);
 std::ostream& operator<<(std::ostream& os, const Student& s); 
 bool operator==(const Student&, const Student&); 
 Student operator+(const Student&, char);
 Student operator+(char, const Student&);

The empty() member function lets the client know whether or not the object if in a safe empty state.

The implementation file for our upgraded Student class contains:

 // Student.cpp

 #include <cstring>
 #include "Student.h"

 Student::Student() {
     no = 0;
     grade[0] = '\0';
 }

 Student::Student(int n) {
     *this = Student(n, "");
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic
     no = n;
     std::strcpy(grade, g);
 }

 void Student::display(std::ostream& os) const {
     os << no << ' ' << grade;
 }

 Student& Student::operator+=(char g) {
     int i = strlen(grade);
     if (i < M) {
         // add validation logic here
         grade[i++] = g;
         grade[i] = '\0';
     }
     return *this;
 }

 std::ostream& operator<<(std::ostream& os, const Student& st){ 
     st.display(os);
     return os;
 }

 std::istream& operator>>(std::istream& is, Student& s) {
     int no;
     char grade[M+1];

     // student number
     std::cout << "Number : ";
     is >> no;

     // student grades
     std::cout << "Grades : ";
     is.ignore();             // swallow newline in the buffer 
     is.getline(grade, M+1);  // read string with whitespace

     Student temp(no, grade);
     if (!temp.empty())
         s = temp;  // replace s only if not empty
     return is;
 }

The getline() member function of the istream object receives in its second parameter the size of the C-style string that accepts the input data, including space for the null byte terminator. 

The client program that uses our upgraded Student class produces the results shown on the right:

 // Custom I/O Operators
 // customIO.cpp

 #include "Student.h"

 int main ( ) {
     Student harry;

     std::cin >> harry;
     std::cout << harry << std::endl;
 }







 Number : 1234
 Grades : ABACA 
 1234 ABACA

This implementation assumes that the input stream does not contain characters that would cause a stream failure.  To handle failures, we include logic that tests the state of the input object and requests corrections from the user as required (see the Robust Validation section in the chapter on Input and Output Examples). 


String Class

The Problem

Determining the amount of memory needed to hold user input requires special attention.  We do not know how much memory to allocate before we have received all of the string input from the user.  We cannot predict at compile time the maximum number of characters that a user will enter.  The string class from the standard library provides a way to determines the number at input-time. 

The Solution

A string object can accept as many characters as the user enters and store them internally.  The helper function getline() extracts them from the input stream.  Its prototype is

 std::istream& getline(std::istream&, std::string&, char);

The first parameter receives a reference to the istream object, the second parameter receives a reference to the string object and the third parameter receives the character delimiter for terminating extraction (newline by default). 

The <string> header file contains the class definition and this prototype.  The string class includes member functions for converting its internal data into a C-style null-terminated string:

  • std::string::length() - returns the number of characters in the string
  • std::string::c_str() - returns the address of the C-style null-terminated version of the string 

Preliminary Example

For example, to extract an unknown number of characters from the standard input stream, we

  • define a string object
  • extract the input using the getline() helper
  • determine the memory required for the C-style null terminated string
  • allocate dynamic memory for the C-style null-terminated string
  • copy the input data from the string object into the allocated memory
 // String class example
 // string.cpp

 #include <iostream>
 #include <string>

 int main( ) {
     char* s;
     std::string str;

     std::cout << "Enter a string : ";
     if (std::getline(std::cin, str)) {
         s = new char [str.length() + 1];
         std::strcpy(s, str.c_str());
         std::cout << "The string entered is : >" << s << '<' << std::endl; 
         delete [] s;
     }
 }

Student Class Example

To enable input of an indefinite number of grade in our Student class, we collect input using a local string object, allocate dynamic memory for grade, and copy the input into the newly allocated memory. 

The header file contains:

 // Student.h

 #include <iostream>

 class Student {
     int no;
     char* grade;
 public:
     Student();
     Student(int, const char*);
     Student(const Student&);
     Student& operator=(const Student&); 
     ~Student();
     bool empty() const { return no == 0; }
     void display(std::ostream&) const;
 };

 std::istream& operator>>(std::istream& is, Student& s);
 std::ostream& operator<<(std::ostream& os, const Student& s); 

The implementation file contains:

 // Student.cpp

 #include <cstring>
 #include <string>
 #include "Student.h"

 Student::Student() {
     no = 0;
     grade = nullptr;
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic
     no = n;
     grade = new char[std::strlen(g) + 1];
     std::strcpy(grade, g);
 }

 Student::Student(const Student& s) {
     grade = nullptr;
     *this = s;
 }

 Student& Student::operator=(const Student& s) {
     if (this != &s) {
         no = s.no;
         delete [] grade;
         if (s.grade != nullptr) {
             grade = new char[std::strlen(s.grade) + 1];
             std::strcpy(grade, s.grade);
         }
         else
             grade = nullptr;
     }
     return *this;
 }

 Student::~Student() {
     delete [] grade;
 }

 void Student::display(std::ostream& os) const { 
     os << no << ' '
        << ((grade != nullptr) ? grade : "");
 }

 std::ostream& operator<<(std::ostream& os, const Student& s) { 
     s.display(os);
     return os;
 }

 std::istream& operator>>(std::istream& is, Student& s) {
     bool ok;
     int number;
     std::string grade;

     // student number
     std::cout << "Number : ";
     is >> number;

     // student grades
     std::cout << "Grades : ";
     is.ignore();
     if (std::getline(is, grade)) {
         Student temp(number, grade.c_str());
         if (!temp.empty())
             s = temp;
     }
     else {
         is.clear();
         is.ignore(2000, '\n');
     }
     return is;
 }

The extraction operator only stores the input in the right operand if the getline() function successfully reads the grade input.

 // String Class
 // string.cpp

 #include <iostream>
 #include "Student.h"

 int main ( ) {
     Student harry;

     std::cin >> harry;
     std::cout << harry << std::endl; 
 }









 Number : 1234
 Grades : ABACA 
 1234 ABACA

Summary

  • we may overload the insertion and extraction operators as helper operators for a compound type
  • the first parameter in the operator definition is a modifiable reference to the stream object
  • returning a modifiable reference to the stream object enables cascading

Exercises



Previous Reading  Previous: Helper Functions Next: Custom File Operators   Next Reading


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