Operator Overloading

- In C++, standard and class member functions can be overloaded
- In addition, a special type of function called an operator function can be overloaded

- Overloaded operator functions allow class objects to be used with standard types of operators

- Overloading operators provide an intuitive and natural way to process objects in applications

 

 

Operators
- Operators are symbols that define specific tasks when used with different variable types

- Standard types of operators include the following

·         Mathematical operators (+, -, *, ...)

·         Relational operators (<, >, ==, ...)

·         Logical operators (&&, ||, !, ...)

·         Access operators ([], ->)

·         Assignment operator (=)

·         Stream operators (<<, >>)

·         Type conversion operators

 

- Operators can be unary (prefix and postfix) or binary

- Unary operators are operators used with one variable, examples include ++, --, !

- Binary operators are operators used with two variables, examples include +, +=, =, >

- Unary operators can be called in prefix (++a) or postfix (a++) forms

 

 

 

Operator Functions

- Functions that can be used to extend the built-in set of C++ operators

- When used with built-in types (float, char, ...) operators have specific meanings

- For example, consider the following program using standard mathematical operators (+, =)

 

#include <iostream>

using namespace std;

 

main()

{

  float a = 10.0;

  float b = 20.0;

  float c;

 

  c = a + b;

}


- The operators
+ and = have pre-defined meanings when used with floating point variables (c, a, b)

- The meanings define how to add (+) the two floats (a,b), then assign (=) the result to the float c

- When used with user-defined classes, however, operators can be given a new meaning
- This new meaning is defined in an associated function called an operator function
- When class objects are used with the operator, this function is called to perform the desired task
- In this way, the meaning of the operator is overloaded for the user-defined class

- Operator functions can be class members, non-members (standard functions), or both

 

 

 

Defining Operator Functions
- The syntax for an operator function is similar to a standard function

 

<return type> operator<op>(<arg1, arg2, ...>)
{
    <definition>

}                         

 

operator<op>    function name (the keyword operator followed by operator symbol)

<return type>  function return type

arg1, arg2,      function arguments

<definition>    function definition

 

- When an operator statement is processed, it is converted into a call to its operator function

- The format of this converted function call depends upon a number of factors

 

·         Is the operator unary or binary

·         If the operator is unary, is it prefix or postfix?

·         Is the operator function a member or nonmember of a class?

 

- The tables below define the ways in which operator statements are converted to operator functions

- The form of the actual function calls are used when defining operator functions in user-defined classes

- Op is the operator in a statement; X and Y represent the left and right operands in a statement

- The term “nonmember” simply refers to a standard function (as opposed to a class method)

- Overloaded operator functions can be written as either member of nonmember functions

- In some cases with mixed arguments (as we’ll see below), we have to implement as nonmember

- Note for member functions, left operand of a binary operator is the object calling the method

- Note for member functions, single operand of a unary operator is the object calling the method

 

 

Binary operator functions

Member Status

Statement

Actual Call

Member

X Op Y

X.operatorOp(Y);

Nonmember

X Op Y

operatorOp(X,Y);

 

 

Unary operator functions

Member Status

Statement

Actual Call

Member

Op X (prefix)

X.operatorOp();

Member

X Op (postfix)

X.operatorOp(int);

Nonmember

Op X (prefix)

operatorOp(X);

Nonmember

X Op (postfix)

operatorOp(X,int);



- Consider the following example where a and b are objects of a user-defined class

 

      a + b

 

- In this example, Op is the + operator and X and Y are the left and right operands a and b

- As the compiler processes this statement, it replaces it with the actual call specified in the table

- Since + is a binary operator, we use the binary operator table to determine the form of the actual call

- Using our table, the statement above is replaced with

 

                        a.operator+( b )                if the operator function is defined as a class member

                       

                        or

                       

                        operator+( a, b )              if the operator function is defined as a non-member

 

- The class member form of this overloaded operator function would be written as follows

 

class SomeClass

{

private:

...

 

public:

 

// Overloaded binary operator +, class member

void operator+( const SomeClass &y )

{

      ....

}

};

 

- The non-member form of this overloaded operator function would be written as follows

 

// Overloaded binary operator +, non-member of class

void operator+( const SomeClass &x, const SomeClass &y )

{

      ...

}

 

- Consider another example involving a unary operator where s is an object of a user-defined class

 

++s

 

- Since ++ is a unary operator, we use the unary operator table to determine the form of the actual call

- It is being used in its prefix form in the above example

- Using our table, the statement above is replaced with

 

s.operator++()                                if the operator function is defined as a class member

 

or

 

operator++( s )                              if the operator function is defined as a non-member

 

 

- The class member form of this overloaded operator function would be written as follows

 

class SomeClass

{

private:

...

 

public:

 

// Overloaded unary operator ++, prefix, class member

void operator++()

{

      ....

}

};

 

- The non-member form of this overloaded operator function would be written as follows

 

// Overloaded unary operator ++, prefix, non-member of class

void operator++( const SomeClass &y )

{

      ...

}

 

 

- Note that to distinguish prefix and postfix for unary function calls, a “dummy” argument is used

- This int argument is not used for anything and is simply used to distinguish function calls

- Unary operators ++/-- can be prefix or postfix, and thus need to be distinguished in their definitions

 

void operator++();            // prefix declaration

void operator++(int);         // postfix declaration

- In our example above, the postfix form of the ++ operator as a class member would be written as follows

class SomeClass

{

private:

...

 

public:

 

// Overloaded unary operator ++, postfix, class member

// Note dummy integer argument

void operator++( int dummy )

{

      ....

}

};

 

- The non-member form of this overloaded operator function would be written as follows

 

// Overloaded unary operator ++, postfix, non-member of class

// Note dummy integer argument

void operator++( const SomeClass &y, int dummy )

{

      ...

}


 

- Consider a user-defined class example defining a clock

- Examples of overloaded operators are implemented as members and non-members of this class

 

class clock
{
private:
 double data; // time in seconds

public:

    // Constructor
    clock(double h=0.0, double m=0.0, double s=0.0 );

    // Convert time to hour, minutes, seconds
    void hms(double &h, double &m, double &s );

    // Overload += operator - MEMBER
    void operator+=(const clock &t);

    // Access
    float getData() { return data; }
    void setData(double s) { data = s; }
};

// Constructor (converts from hms to seconds)
clock::clock(double h, double m, double s)
{
    data = h*3600.0 + m*60.0 + s;
}

// Converts seconds to hms
void clock::hms(double &h, double &m, double &s)
{
    h = (double)((int)data/3600);
    s = data - h*3600.0;
    m = (s/60.0);
    s -= m*60.0;
}

// += operator to add one time to another - MEMBER
void clock::operator+=(const clock &t)
{
    data += t.data;  // add t's time to itself
}
 

// Overloaded addition operator - NONMEMBER
clock operator+(clock &x, clock &y)
{
  clock t;
  t.setData( x.getData() + y.getData() );
  return t;
}

#include <stdio.h>

main()
{
    double h,m,s;
    clock rolex, casio(12,0,0), timex(3,30,0);

    // Use overloaded + operator (nonmember)
    rolex = casio + timex;
    rolex.hms(h,m,s);
    printf("Time is %02.0f:%02.0f:%02.0f\n", h, m, s );

    // Use overloaded += operator (member)
    rolex += timex;
    rolex.hms(h,m,s);
    printf("Time is %02.0f:%02.0f:%02.0f\n", h, m, s );
}
 

Output
Time is 15:30:00
Time is 19:00:00

- Note that the statements

rolex = casio + timex;
rolex += timex;

- Are translated into the following expressions by the compiler during processing

rolex = operator+(casio, timex);
rolex.operator+=(timex);

- Note also how overloaded operator function return objects
- This allows us to perform multiple operations

clock indigo = rolex + timex + casio + timex + casio;


 

Different Argument Types
- Operator functions define new meanings when used with user-defined class objects
- One major problem can occur, however, when using binary operators and different operand types
- Consider the following example (strcpy and strcat are C functions to copy and concatenate strings)

#include <iostream>

using namespace std;

 

class MyString
{
private:
    char str[200];

public:
    MyString( const char *s="") { strcpy(str,s); }
    MyString operator+( MyString &s1 ) { return strcat(str,s1.str); }
    void Print() { cout << str << endl; }
};

main()
{
    MyString str1( "Hello" ), str2( " World" );
    MyString str3 = 1 + str2;        // compile error
    MyString str4 = str2 + 1;        // compile error
}

 

Compilation Error

In function `int main()':

error: no match for 'operator+' in '1 + str2'

error: no match for 'operator+' in 'str2 + 1'

error: candidates are: MyString MyString::operator+(MyString&)

 


- Let’s try to analyze the problem; using our tables, the last statements are translated into the following member functions

 

MyString str3 = 1.operator+(str2);    // ??
MyString str4 = str2.operator+(1);    // ??

 

- In the first statement, the left operand 1 is not a MyString object!
- How can we use an integer as the left operand? (we can't)

- Instead, we implement this operator function as a non-member function

 

#include <iostream>

using namespace std;

 

class MyString

{

private:

  char str[200];

 

public:

  MyString( const char *s="") { strcpy(str,s); }

 

  // Print method

  void Print() { cout << str << endl; }

 

  // Access methods

  char *getString() { return str; }

  void setString( char *s ) { strcpy( str, s ); }

};

 

// Non-member overloaded operator functions

MyString operator+(MyString &s1, MyString &s2)

{

  return MyString( strcat(s1.getString(),s2.getString()) );

}

 

MyString operator+(int a, MyString &s2)

{

  char s[200];

  sprintf( s, "%d %s\n", a, s2.getString() );

  return MyString( s );

}

 

MyString operator+(MyString &s2, int a)

{

  char s[200];

  sprintf( s, "%s %d\n", s2.getString(), a );

  return MyString( s );

}

 

main()

{

  MyString str1("Hello"), str2(" World");

  MyString str3 = 1 + str2;

  MyString str4 = str2 + 1;

  str3.Print();

  str4.Print();

 

Output

1 World

World 1

 

- With these additions to our class, the statement

 

MyString str3 = 1 + str2;

 

   translates into the following legal function call

 

MyString str3 = operator+( 1, str2 );


- Note how using nonmember functions allow us to explicitly define our first argument
- Note we added an overloaded function with an integer and string

- Another common method is to use the friend keyword when defining non-member functions

- Functions that are declared as a friend to a class have access to the private information of that class

- Consider the following revisions to our MyString class

 

 

#include <iostream>

using namespace std;

 

class MyString

{

private:

  char str[200];

public:

  MyString( const char *s="") { strcpy(str,s); }

 

  // Print method

  void Print() { cout << str << endl; }

 

  // Access methods

  char *getString() { return str; }

  void setString( char *s ) { strcpy( str, s ); }

 

  // Non-member, friend overloaded operator functions

  friend MyString operator+(const MyString &s1, const MyString &s2);

  friend MyString operator+(int a, const MyString &s2);

  friend MyString operator+(const MyString &s2, int a);

};

 

// Non-member, friend overloaded operator functions

MyString operator+(const MyString &s1, const MyString &s2)

{

  char str[500];

  sprintf( str, "%s %s", s1.str, s2.str );

  return MyString( str );

}

MyString operator+(int a, const MyString &s2)

{

  char s[200];

  sprintf( s, "%d %s\n", a, s2.str );

  return MyString( s );

}

MyString operator+(const MyString &s2, int a)

{

  char s[200];

  sprintf( s, "%s %d\n", s2.str, a );

  return MyString( s );

}

main()

{

  MyString str1("Hello"), str2(" World");

  MyString str3 = 1 + str2;

  MyString str4 = str2 + 1;

  str3.Print();

  str4.Print();


 

Output

1 World

World 1

 

- With this revised class, the statement

 

MyString str3 = 1 + str2;

 

   translates into correctly into the following legal function call

 

MyString str3 = operator+( 1, str2 );

 

- Note how using nonmember functions allow us to explicitly define our first argument
- Note we added an overloaded function with an integer and string
- Note how using
friend functions allow us to access the internal information of the class objects
- Note the use of the
const keyword to prevent modification to an object

- Note when the function can be implemented as a member or non-member, the choice is left to the programmer

 

Returning Objects
- A goal when using operators is to be able to use them in manner similar to mathematical operators

- As in mathematical expressions, we’d like to be able to write the following statement

 

a = ++b;

 

- Note that the ++ operator function must return an object (to assign to a) for this statement to operate correctly

- For this reason, it’s important to consider return objects when writing overloaded operator functions

- Let’s consider an example with a class defining a Point

 

#include <iostream>

using namespace std;

class Point
{
private:
  float x, y, z;

public:
  Point( float a=0.0, float b=0.0, float c=0.0 )
  {
    x = a; y = b; z = c;
  }

  // Print method
  void Print()
  {
    cout << x << “, “ << y << “, “ << z << endl;
  }

  // Overloaded prefix operator member function (no return object)
  void operator++()
  {
    x+=1.0;
    y+=1.0;
    z+=1.0;
  }
};

main()
{
  Point p(1,1,1);
  ++p;
  p.Print();
}
 

Output
2.00 2.00 2.00


- This program works fine as long as the Point object is not on the right side of an assignment
- The operation,
++p, should return the modified Point object if on the right side of an assignment

- We would expect our Point object to work in a manner similar to float variables in the following example

 

#include <iostream>

using namespace std;

main()
{
  float f1 = 1.0;
  float f2 = ++f1;
  cout << “f2: “ << f2 << endl;
}

Output
f2: 2.00


- To accomplish, let's modify our operator function to return the modified Point object

#include <iostream>

using namespace std;

class Point
{
private:
  float x, y, z;

public:
  Point( float a=0.0, float b=0.0, float c=0.0 )
  {
    x = a; y = b; z = c;
  }

  // Print method
  void Print()
  {
    cout << x << “, “ << y << “, “ << z << endl;
  }

  // MODIFIED Overloaded prefix operator member function
  Point operator++()
  {
    return Point( x+=1.0, y+=1.0, z+=1.0 );
  }
};

main()
{
  Point p(1,1,1);
  Point p1 = ++p;
  p1.Print();
}
 

Output
2.00 2.00 2.00


- Note how we can return a "nameless" object by returning the constructor result

return Point( x+=1.0, y+=1.0, z+=1.0 );


- Although this works fine, do we really need to return a temporary object at all?
- The modified object already exists (itself) and has the right values, why not return it?
- We can dereference the this pointer and return the modified object itself
- Returning the modified object avoids unneeded memory for temporary object

#include <iostream>

using namespace std;

class Point
{
private:
  float x, y, z;

public:
  Point( float a=0.0, float b=0.0, float c=0.0 )
  {
    x = a; y = b; z = c;
  }

  // Print method
  void Print()
  {
   cout << x << “, “ << y << “, “ << z << endl;
  }

  // MODIFIED Overloaded prefix operator member function
  Point operator++()
  {
    x+=1.0;
    y+=1.0;
    z+=1.0;
    return *this;
  }
};

main()
{
  Point p(1,1,1);
  Point p1 = ++p;
  p1.Print();
}
 

Output
2.00 2.00 2.00


- Note how we are assigning one object to another

    Point p1 = ++p;

- Isn't the assignment symbol (=) an operator in addition to the ++ operator?
- Shouldn't we be overloading the function for this operator also?
- As we’ll see later, assignment overloading involves implementing methods to copy objects