Miscellaneous Operators (<<, >>, [], ())

Discover the power of special ostream operators (<<, >>) that enable customized printing of user-defined data types using cout on both the console and file stream.

Overloading I/O operators

The I/O (Input/Output) objects cin/cout don't work for user-defined data types. Overloading I/O operators refers to the process of defining and customizing the operators (<< and >>) for user-defined data types. This allows us to control how our objects are formatted when outputting to a console or file streams or how they are read from a stream (iostream/ifstream).

When overloading I/O operators, we have two approaches: friend functions and global functions. We use friend functions when we need access to the private members of the class. To achieve this, we declare them as friends using the friend keyword, in their prototypes within the class and then define them outside the class. (We can define them inside the class as well.) This ensures that the functions can effectively work with the private members while maintaining proper encapsulation. As a result, we can elegantly customize I/O operations for user-defined data types in a clean and efficient manner.

For example, to overload the output operator (<<) for a user-defined MyClass class, we can define a friend function as follows:

#include <iostream>
#include <string>
using namespace std;
class Person
{
string name;
int age;
public:
Person(const string& name, int age) : name(name), age(age)
{}
friend ostream& operator<<(ostream& out, const Person& person);
// Prototype of the function
};
// Defining the friend function (which has access to private data members)
ostream& operator<<(ostream& out, const Person& person)
{
out << "Name: " << person.name << ", Age: " << person.age;
return out;
}
int main()
{
Person p("John Doe", 25);
cout << p << endl;
/*
// Exercise: Uncomment me
cin>>p; // User should be able to enter : Maria 36
cout << p; // modified p should be displayed : Name: Maria, Age: 36
*/
return 0;
}

In the friendFunction.cpp file:

We implement the operator<<() function using the friend keyword.

  • Lines 6–9: A class called Person is defined with private member variables called name and age.

  • Line 15: The ostream& operator<<() function prototype is declared. Notice that this is just a prototype.

  • Lines 20–24: Inside the implementation of the operator<<() function, the name and age variables of the Person class object p are formatted and outputted to the ostream using the << operator.

  • Line 23: The operator<<() function returns a reference to the ostream object, allowing for cascading, which means that multiple << operations can be chained together.

  • Line 28: Using the cout object and the overloaded << operator, the p object is printed to the console.

Note: The cascading effect is achieved by the returned reference to the ostream object, enabling additional output statements to be chained with the << operator, allowing multiple pieces of information to be printed on a single line.

In the globalFunction.cpp file:

  • The only difference in this implementation is that it uses getters to implement the printing of the Person class object. Therefore, the operator<<() function enables custom printing of the Person class objects by overloading the << operator. It takes an ostream& as the output stream and a const Person& as the object to be printed. The function returns a reference to the ostream object, facilitating cascading. In the main() function, the overloaded operator<<() is used with cout to print the Person class object, showcasing cascading by appending additional output statements. Similar to this, we can easily overload an input stream operator >> as well (both for console and file). We’re leaving that as a practice exercise for you.

Exercise: Overloading the >> operator (adding the operator>>() function)

Write two separate implementations using friend and global functions to enable taking a person as input from the user. If you have trouble getting to the solution, you can click the “Show Hint” button to see how to implement it.

I/O operator overloading and adding new features in cout

We already have used istream global object cin and ostream global object cout for primitive data types, usually using the following syntax:

cin >> primitive_DataType_Variable1 >> primitive_DataType_Variable1;
cout << primitive_DataType_Variable1 << primitive_DataType_Variable2;

Their behavior is actually defined in the C++ standard libraries (istream and ostream). We can build on that and add further functionality. For example, what if instead of writing the parameter to be printed to the right of the cout statement, we want to add the functionality that we would like to print with the following syntax?

primitive_DataType_Variable >> cin;
primitive_DataType_Variable1 << cout;

What can we do to add this functionality using the already given functionality of the cin >> and cout << statements?

Here’s what we can do:

Press + to interact
#include <iostream>
using namespace std;
// Overloading the insertion operator (<<) for integers
ostream& operator<<(const int& v, ostream& out)
{
out << v; // Stream the integer value into the output stream
return out; // enabling the cascading
}
int main()
{
int var = 20;
var << cout << " is the value"; // Outputs: 20 is the value
// This will call the above operator function
// and then due to cascading it will display the string
cout << var << " is the value"; // Outputs: 20 is the value
/*
// Fun exercise: Add the functionality for printing the following line
cout << "Enter new value: ";
var >> cin;
cout << "New Value is; " << var << endl;
*/
return 0;
}
  • The code defines a new version of the insertion operator << that takes an integer (const int&) as the left operand and an output stream (ostream&) as the right operand. This version of the operator is a nonmember function (global function) since it is defined outside the class.

  • Inside the overloaded operator function, the integer value v is streamed into the output stream (out) using the out << v; statement. This allows us to customize how integers are displayed in the output stream. The function then returns the output stream out to enable cascading, allowing us to chain multiple insertion operations.

  • In the main function, an integer variable var is initialized with 20 as its value. The overloaded insertion operator is utilized twice to print the value of the variable and a custom message:

    • var << cout << " is the value";: This code line outputs the 20 is the value string. First, the overloaded insertion operator is called with var as the left operand, and cout (the output stream) as the right operand. The value of var (i.e., 20) is streamed into cout. Then, the custom message is the value is displayed, resulting in the combined output by calling the already present cout << string_message already overloaded (due to cascading, the return out statement).

  • We have added a practice exercise on changing the order for cin as well. You’re encouraged to try it for fun.

The provided overloaded insertion operator for integers enables us to employ a new syntax for displaying integers, enhancing the versatility and expressiveness of the cout statement. This demonstrates the power of operator overloading, which allows us to define custom behavior for operators, introducing functionalities that were not present before.

However, it’s essential to note that operator overloading doesn’t allow us to override the existing implementations of operators, such as adding two integers or floats. We can’t change the fundamental behavior of standard operators like +, -, *, and so on, for primitive data types. Instead, we can only define custom behaviors for these operators when used with our user-defined data types, offering a way to handle data in a manner that suits our specific needs.

Miscellaneous operators

In this topic, we’ll discuss two miscellaneous operators: the subscript operator ([]), the function call operator (()). These operators serve unique purposes and can be overloaded to provide custom behavior within a class.

  • 1. Subscript operator ([]): The subscript operator ([]) allows objects to be accessed using array-like syntax. It's commonly used to access elements within a class that represent a collection or a sequence. By overloading this operator, we can define custom behavior for accessing and modifying elements of an object. Suppose we have a class Date that represents a date with day, month, and year components. We can overload the subscript operator to allow access to these components using the subscript notation.

  • 2. Function call operator (()): The function call operator (()) enables objects to be invoked or called as if they were functions. By overloading this operator, we can define custom behavior for invoking an object as a function.

Let’s look at an example to cover these two operators.

Press + to interact
#include <iostream>
using namespace std;
class Date
{
private:
int day;
int month;
int year;
public:
Date(int d, int m, int y) : day(d), month(m), year(y)
{}
int& operator[](int index)
{
if (index == 0)
{
return day;
}
else if (index == 1)
{
return month;
}
else if (index == 2)
{
return year;
}
else
{
throw out_of_range("Invalid index for Date");
}
}
void operator()()
{
cout << "Today's date is: " << day << "/" << month
<< "/" << year << endl;
}
};
int main() {
Date date(14, 6, 2023);
// Using the subscript operator
cout << "Day: " << date[0] << endl;
cout << "Month: " << date[1] << endl;
cout << "Year: " << date[2] << endl;
// Using the function call operator
date();
// Modifying the date using the subscript operator
date[0] = 15;
date[1] = 7;
date[2] = 2023;
date(); // Updated date
return 0;
}

In summary, there are several other operators that we can overload, like unary operators ('-', '!'). There are several other bitwise operators too, like &, |, ^, ~, &=, |=, ^=, ~=, and so on. We may use them appropriately by looking at their parameter requirements and defining them in their own fashion, however needed.