TUTORIAL: Defining and Overloading Operators in C++

  • spork
  • Brewmaster
  • Silver Member
  • User avatar
  • Posts: 6254
  • Loc: Seattle, WA

Post 3+ Months Ago

Introduction

Note: This tutorial assumes that you already have a moderate understanding of the C++ language. You should be able to write and compile simple programs, and you should be familiar with object-oriented programming and how classes work in C++.

Note: The complete source code for the examples in this tutorial can be downloaded at the end of the tutorial.

One of the powerful features of C++ is the ability to overload operators. Almost every operator in C++ can be overloaded to provide custom functionality that will give your classes greater flexibility and allow then to behave as other programmers would expect them to.

In this tutorial, you will learn what operators are, how they can be overloaded, and some common reasons for overloading various operators.

What is an operator in C++?

Operators are functions. Specifically, operators are special functions that are used to perform operations on data without directly calling methods each time. Common operators that you should be familiar with include +, -, *, /, <<, >>, and so on. All such operators can be used on primative data types in C++ to achieve certain results. For example, we can use the + operator to add together two numeric values, such as ints, floats, and doubles:
CPP Code: [ Select ]
int result = 2 + 4;

In the code above, two operators are used: the = operator and the + operator. The + operator adds together the two integer values, returning the result. The = operator takes the resulting value on the right and assigns it to the variable on the left.

We also see operators used in other areas, such as writing formatted data to a stream:
CPP Code: [ Select ]
cout << "Brownies are better with sprinkles.";

In the code above, the insertion operator is used to insert data into the output stream.

It is important to note that there are two important types of operators: unary and binary.

Unary operators are operators that act on a single piece of data. Look at the following code:
CPP Code: [ Select ]
int myInt = 5;
myInt++;  // ++ is a unary operator
myInt--;  // -- is also a unary operator
  1. int myInt = 5;
  2. myInt++;  // ++ is a unary operator
  3. myInt--;  // -- is also a unary operator

In the above example, operator ++ and operator -- are unary operators, because they function on only one piece of data, in this case, the int variable myInt.

Binary operators are operators that act on a two pieces of data. Look at the following code:
CPP Code: [ Select ]
float numerator = 5.376;
float denominator = 55.372;
float result = numerator / denominator;  // "/" is a binary operator
result = denominator + numerator;  // + is also a binary operator
  1. float numerator = 5.376;
  2. float denominator = 55.372;
  3. float result = numerator / denominator;  // "/" is a binary operator
  4. result = denominator + numerator;  // + is also a binary operator

In the above example, operator / and operator + are binary operators, because they function on two pieces of data, in this case, the float variables numerator and denominator.

What operators can be overloaded?

The following operators can be overloaded:
Source: Wikipedia

    + Unary Plus
    + Addition (Sum)
    ++ Prefix Increment
    ++ Postfix Increment
    += Assignment by Addition
    - Unary Minus (Negation)
    - Subtraction (Difference)
    -- Prefix Decrement
    -- Postfix Decrement
    -= Assignment by Subtraction
    * Multiplication (Product)
    *= Assignment by Multiplication
    / Division (Quotient)
    /= Assignment by Division
    % Modulus (Remainder)
    %= Assignment by Modulus

    < Less Than
    <= Less Than or Equal To
    > Greater Than
    >= Greater Than or Equal To
    != Not Equal To
    == Equal To
    ! Logical Negation
    && Logical AND
    || Logical OR

    << Bitwise Left Shift
    <<= Assignment by Bitwise Left Shift
    >> Bitwise Right Shift
    >>= Assignment by Bitwise Right Shift
    ~ Bitwise One's Complement
    & Bitwise AND
    &= Assignment by Bitwise AND
    | Bitwise OR
    |= Assignment by Bitwise OR
    ^ Bitwise XOR
    ^= Assignment by Bitwise XOR

    = Basic Assignment
    () Function Call
    [] Array Subscript
    * Indirection (Dereference)
    & Address-of (Reference)
    -> Member by Pointer
    ->* Member by Pointer Indirection

    (type) Cast
    , Comma
    sizeof Size-of
    typeid Type Identification
    new Allocate Storage
    new Allocate Storage (Array)
    delete Deallocate Storage
    delete Deallocate Storage (Array)

Why would I want to overload an operator?

Operators are often overloaded to allow custom types to behave like and be used like primative types. It gives your classes a lot of flexibility in terms what how they interact with the rest of your program. In a minute, we'll take a look at an example class and how we can use overloaded operators to enhance it's functionality.

Member vs. Non-member operators

Before we dig in, it is important to note that there are two groups of operators in terms of how they are implemented: member operators and non-member operators. The distinction between the two is the same as it is with methods and functions.

Member operators are operators that are implemented as member functions (methods) of a class.

Non-member operators are operators that are implemented as regular, non-member functions.

Some operators are required to be member operators, others must be non-member operators, and some can be both.

A basic example: Fraction

For the rest of this tutorial, we will be working with a custom class: Fraction. The class declaration for fraction looks like this:
CPP Code: [ Select ]
#include <iostream>
 
class Fraction {
 
private:
   int numerator;
   int denominator;
 
public:
   Fraction( int num, int denom );
   void print( const std::ostream& os ) const;
 
};
  1. #include <iostream>
  2.  
  3. class Fraction {
  4.  
  5. private:
  6.    int numerator;
  7.    int denominator;
  8.  
  9. public:
  10.    Fraction( int num, int denom );
  11.    void print( const std::ostream& os ) const;
  12.  
  13. };


Fraction is a simple class that represents a number as a fraction containing a numerator and a denominator. These are set via the constructor as seen in the declaration above. This tutorial assumes basic knowledge of C++ and classes, so I won't show the definitions for the constructor and the print method. You can assume that print() will output the fraction in the form "2/5".

To create a new Fraction object, we would write the following:
CPP Code: [ Select ]
Fraction f( 2, 5 );  // create the fraction "2/5"


To print the fraction to standard output, we would write:
CPP Code: [ Select ]
f.print( std::cout );


As it stands right now, our Fraction class isn't very useful. There's no way to change the value of our fraction once it's been created, thus it will always be used as a constant. In a moment, we will fix this.

Syntax

The syntax for overloading operators is the same as it is for declaring a function or method:

return-type operator operator-to-overload ( parameters );

Since operators are functions, they can in essence be used in their functional form. For example:
CPP Code: [ Select ]
int result = operator+( 2, 5 );

would do exactly the same thing as
CPP Code: [ Select ]
int result = 2 + 5;


Obviously, we would never write the first form when writing programs, since it defeats the entire point of using operators in the first place. However, it is important to understand how the compiler treats operators so that we can more easily overload them.

Defining and overloading + and +=

The first operator we want to overload is the + operator. We want to be able to add other fractions to each other. In this case, we can use a member operator, so lets add its declaration to our class:
CPP Code: [ Select ]
#include <iostream>
 
class Fraction {
 
private:
   int numerator;
   int denominator;
 
public:
   Fraction( int num, int denom );
   void print( const std::ostream& os ) const;
   Fraction operator+( const Fraction& other ) const;
 
};
  1. #include <iostream>
  2.  
  3. class Fraction {
  4.  
  5. private:
  6.    int numerator;
  7.    int denominator;
  8.  
  9. public:
  10.    Fraction( int num, int denom );
  11.    void print( const std::ostream& os ) const;
  12.    Fraction operator+( const Fraction& other ) const;
  13.  
  14. };


Let's look at each piece of that declaration individually:

Fraction is the return type for this method. We are returning a new Fraction object containing the result of the addition operation. We do this to allow operator chaining, which allows us to write something like this:

CPP Code: [ Select ]
// Declare four fractions
Fraction a(1, 2), b(2, 5), c(2, 3), d(8, 9);
 
// Add a, b, and c together and assign to d
// Operator chaining allows us to write a + b + c
d = a + b + c;
  1. // Declare four fractions
  2. Fraction a(1, 2), b(2, 5), c(2, 3), d(8, 9);
  3.  
  4. // Add a, b, and c together and assign to d
  5. // Operator chaining allows us to write a + b + c
  6. d = a + b + c;


In the code above, the program will add a + b and return a new Fraction object containing the result. The result is then added with c, and the result of that operation is then assigned to d.

operator+ signifies the operator to be overloaded. Note that operator is a keyword in C++; whitespace between it and the actual operator to overload is optional.

const Fraction& other is the single parameter for this method. We take a constant reference to a Function object, because we don't want to ever modify the other Fraction, only the one being added to. We use a Fraction reference rather than a Fraction to avoid making an unnecessary copy each time we add.

Now let's take a look at the definition for this method:
CPP Code: [ Select ]
Fraction Fraction::operator+( const Fraction& other ) const {
   
   // Find the least common multiple of the denominators
   int lcm = other.denominator;
   while( lcm % denominator != 0 ) {
      lcm += other.denominator;
   }
   
   // Return a new fraction with the results
   return Fraction(
      (lcm / denominator) * numerator +
      (lcm / other.denominator) * other.numerator, lcm );
}
  1. Fraction Fraction::operator+( const Fraction& other ) const {
  2.    
  3.    // Find the least common multiple of the denominators
  4.    int lcm = other.denominator;
  5.    while( lcm % denominator != 0 ) {
  6.       lcm += other.denominator;
  7.    }
  8.    
  9.    // Return a new fraction with the results
  10.    return Fraction(
  11.       (lcm / denominator) * numerator +
  12.       (lcm / other.denominator) * other.numerator, lcm );
  13. }


In the method, we find the LCM of the two denominators, and add in the appropriate values from the other function. As mentioned earlier, we return a reference to the current Fraction to allow chaining.

So now we have a way of adding two Fractions together. Fantastic! But wait, what if we want to add whole numbers to our Fractions? It's mighty inconvenient to have to create a new Fraction every time we want to add an integer amount to our Fraction objects. Let's go ahead and add another version of the + operator.

Add the declaration:
CPP Code: [ Select ]
#include <iostream>
 
class Fraction {
 
private:
   int numerator;
   int denominator;
 
public:
   Fraction( int num, int denom );
   void print( const std::ostream& os ) const;
   Fraction operator+( const Fraction& other ) const;
   Fraction operator+( int num ) const;
 
};
  1. #include <iostream>
  2.  
  3. class Fraction {
  4.  
  5. private:
  6.    int numerator;
  7.    int denominator;
  8.  
  9. public:
  10.    Fraction( int num, int denom );
  11.    void print( const std::ostream& os ) const;
  12.    Fraction operator+( const Fraction& other ) const;
  13.    Fraction operator+( int num ) const;
  14.  
  15. };


And define the method:
CPP Code: [ Select ]
Fraction Fraction::operator+( int num ) const {
   
   // Return a new fraction with the results
   return Fraction( numerator + denominator * num, denominator );
}
  1. Fraction Fraction::operator+( int num ) const {
  2.    
  3.    // Return a new fraction with the results
  4.    return Fraction( numerator + denominator * num, denominator );
  5. }


Now that we've overloaded the + operator with another version accepting an integer, we are able to write things like this:
CPP Code: [ Select ]
Fraction f( 3, 10 );  // Create the fraction "3/20"
f = f + 5               // f is now "53/10"
  1. Fraction f( 3, 10 );  // Create the fraction "3/20"
  2. f = f + 5               // f is now "53/10"


At this point, we can easily add the += operator to our class. Let's add the declarations to our class:
CPP Code: [ Select ]
#include <iostream>
 
class Fraction {
 
private:
   int numerator;
   int denominator;
 
public:
   Fraction( int num, int denom );
   void print( const std::ostream& os );
   Fraction operator+( const Fraction& other ) const;
   Fraction operator+( int val ) const;
   Fraction& operator+=( const Fraction& other ) const;
   Fraction& operator+=( int val ) const;
 
};
  1. #include <iostream>
  2.  
  3. class Fraction {
  4.  
  5. private:
  6.    int numerator;
  7.    int denominator;
  8.  
  9. public:
  10.    Fraction( int num, int denom );
  11.    void print( const std::ostream& os );
  12.    Fraction operator+( const Fraction& other ) const;
  13.    Fraction operator+( int val ) const;
  14.    Fraction& operator+=( const Fraction& other ) const;
  15.    Fraction& operator+=( int val ) const;
  16.  
  17. };


Notice that the declarations for += look very similar to +. Let's look at a few key differences.

This time, our return type is Fraction&, a reference to a Fraction object. We use this because the result of the += operation should be a reference back to the original object being added to. Again, this is to allow operator chaining. Consider the following code:
CPP Code: [ Select ]
Fraction a(2, 5), b(6, 8), c(4, 7);
a += b += c;
  1. Fraction a(2, 5), b(6, 8), c(4, 7);
  2. a += b += c;


In the code above, the value of c is added to the value of b. The new value of b is then added to the value of a. Returning references to the objects in the += operator allows us to modify each one in the chain as needed.

We also do not declare these methods as const, since they will directly be modifying their objects.

Now lets look at the code for our += operators:
CPP Code: [ Select ]
Fraction& Fraction::operator+=( const Fraction& other ) {
   
   // Use the + operator we created
   Fraction temp( *this );
   temp = temp + other;
   
   numerator = temp.numerator;
   denominator = temp.denominator;
   
   // Return a reference to ourself
   return *this;
}
  1. Fraction& Fraction::operator+=( const Fraction& other ) {
  2.    
  3.    // Use the + operator we created
  4.    Fraction temp( *this );
  5.    temp = temp + other;
  6.    
  7.    numerator = temp.numerator;
  8.    denominator = temp.denominator;
  9.    
  10.    // Return a reference to ourself
  11.    return *this;
  12. }


In this implementation, very little code is required since we can use the + operator that we previously defined. The same is true for our "integer version":
CPP Code: [ Select ]
Fraction& Fraction::operator+=( int val ) {
   
   // Use the + operator we created
   Fraction temp( *this );
   temp = temp + val;
   
   numerator = temp.numerator;
   denominator = temp.denominator;
   
   // Return a reference to ourself
   return *this;
}
  1. Fraction& Fraction::operator+=( int val ) {
  2.    
  3.    // Use the + operator we created
  4.    Fraction temp( *this );
  5.    temp = temp + val;
  6.    
  7.    numerator = temp.numerator;
  8.    denominator = temp.denominator;
  9.    
  10.    // Return a reference to ourself
  11.    return *this;
  12. }


Now, we can write things like the following:
CPP Code: [ Select ]
Fraction a(3, 5);
a += a;  // Add the value of a to a (double it)
a += 4;  // Add 4 to a
  1. Fraction a(3, 5);
  2. a += a;  // Add the value of a to a (double it)
  3. a += 4;  // Add 4 to a


Taking things a step further: <<

Now that you're comfortable with implementing some basic member operators, lets take a look at an example of an operator that must be implemented as a non-member function: <<

The << operator is used for two things in C++: to perform a bitwise left shift, and to insert data into a output stream. We will only be dealing with the latter.

At the moment, we have a way of printing our class to an output stream: print(). We pass in an output stream and the fraction gets printed for us. For example:
CPP Code: [ Select ]
Fraction f(2, 5);
f.print(std::cout);
  1. Fraction f(2, 5);
  2. f.print(std::cout);

Will output the following on standard output:
CPP Code: [ Select ]
2/5


This is fine for minimal usage of the class. But what if we wanted to include our Fraction as part of a more complicated formatted insertion into a stream?
CPP Code: [ Select ]
Fraction f(2, 5);
std::cout << "A string here " << f << " another string, etc." << std::endl;
  1. Fraction f(2, 5);
  2. std::cout << "A string here " << f << " another string, etc." << std::endl;


If we want to be able to insert Fractions into streams in this manner, we must define the << operator, or insertion operator. Let's go ahead and declare it in our class:
CPP Code: [ Select ]
#include <iostream>
 
class Fraction {
 
private:
   int numerator;
   int denominator;
 
public:
   Fraction( int num, int denom );
   void print( const std::ostream& os );
   Fraction operator+( const Fraction& other ) const;
   Fraction operator+( int val ) const;
   Fraction& operator+=( const Fraction& other ) const;
   Fraction& operator+=( int val ) const;
 
};
 
ostream& operator<<( ostream& os, const Fraction& f );
  1. #include <iostream>
  2.  
  3. class Fraction {
  4.  
  5. private:
  6.    int numerator;
  7.    int denominator;
  8.  
  9. public:
  10.    Fraction( int num, int denom );
  11.    void print( const std::ostream& os );
  12.    Fraction operator+( const Fraction& other ) const;
  13.    Fraction operator+( int val ) const;
  14.    Fraction& operator+=( const Fraction& other ) const;
  15.    Fraction& operator+=( int val ) const;
  16.  
  17. };
  18.  
  19. ostream& operator<<( ostream& os, const Fraction& f );


But wait! The declaration for this function is outside of our class, that is, it's a non-member function! In fact, this operator must be operated as a non-member function. The reason is relatively straightforward once you understand how the operator is called. Remember that a line like the following:
CPP Code: [ Select ]
Fraction f( 2, 5 );
std::cout << f;
  1. Fraction f( 2, 5 );
  2. std::cout << f;

is actually called like this:
CPP Code: [ Select ]
operator<<( std::cout, f );


To write the << operator as a method of Fraction, the output stream would have to be the single argument to the method, and the Fraction object would have to be on the left hand side, like this:
CPP Code: [ Select ]
Fraction f( 2, 5 );
f << std::cout;
  1. Fraction f( 2, 5 );
  2. f << std::cout;


This is obviously incorrect, since the output stream must always be on the left hand side of the insertion operator. Thus, << would have to be implemented as a member function of std::ostream. Since we cannot modify standard library classes, our only option is to implement << as a non-member function taking two arguments.

Let's look at the code for operator <<:
CPP Code: [ Select ]
std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
   
   os << f.numerator << '/' << f.denominator;
 
   return os;
}
  1. std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
  2.    
  3.    os << f.numerator << '/' << f.denominator;
  4.  
  5.    return os;
  6. }


This implementation seems valid, but there's a problem. Numerator and denominator are private members of Fraction. Because of this, non-member functions such as our << operator do not have access to them. We could write accessors for the data members, however this is unnecessary and duplicates code. Looking back at our class declaration, we see that we already have a nice method to print out a Fraction to an output stream: print(). Since this method is public, let's go ahead and use it in our << operator:
CPP Code: [ Select ]
std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
   
   // Use the member function to print to the output stream
   f.print( os );
 
   return os;
}
  1. std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
  2.    
  3.    // Use the member function to print to the output stream
  4.    f.print( os );
  5.  
  6.    return os;
  7. }


Now we have one single piece of code, contained in our class, that determines how Fractions are displayed. If we ever want to change this, we simply change it in print(), and our << operator will sill work correctly.

Style and usage guidelines

One last thing to address on the subject of operator overloading is style and usage. It can be very tempting for beginners to start overloading all sorts of operators in their classes, sometimes for purposes not intended by that operator. For example, one might be creating a List class and choose to overload the increment operator (++) to automatically add an element to the list. This can be done, and yes, it will work. But it doesn't make sense. The ++ operator is not meant for this purpose, and even though it may seem convenient, this type of functionality should be avoided.

Conclusion

You should now have a solid understanding of how operators work in C++ and how they can be defined and overloaded to allow your classes to function more like primative data types. You should understand the different between unary and binary operators, and well as when to implement operators as member functions versus non-member functions.

I always welcome questions or feedback about this tutorial. Simply post a reply or PM me; I'm glad to help!
Attachments:
Fraction.zip

(1.76 KiB) Downloaded 7787 times

Complete source code for the Fraction example used in this tutorial.

  • joebert
  • Fart Bubbles
  • Genius
  • User avatar
  • Posts: 13504
  • Loc: Florida

Post 3+ Months Ago

I was browsing around today & stumbled upon an operator PHP PECL package & had little idea what good it would be, but I remembered seeing this tutorial & just now finished reading it.


Great read, I've already got thoughts along these lines

Code: [ Select ]
baasket += new item(...);
  • spork
  • Brewmaster
  • Silver Member
  • User avatar
  • Posts: 6254
  • Loc: Seattle, WA

Post 3+ Months Ago

Very cool Joebert. I've always been hopeful that operator overloading would eventually be added in PHP6, but it looks as if they have no intention of adding it anytime soon, if at all.

Quote:
There's an operator overloading extension, see pecl.php.net -> search for "operator" but such a feature won't go in PHP's core.

http://bugs.php.net/bug.php?id=9331

The PECL operator package looks promising, I might have to take that for a spin this week.
  • genux
  • Graduate
  • Graduate
  • User avatar
  • Posts: 106
  • Loc: UK

Post 3+ Months Ago

Great tutorial spork :).
  • spork
  • Brewmaster
  • Silver Member
  • User avatar
  • Posts: 6254
  • Loc: Seattle, WA

Post 3+ Months Ago

Thanks!
  • alpy04
  • Born
  • Born
  • alpy04
  • Posts: 1

Post 3+ Months Ago

Your tutorial is really easy to understand. Great work Spook.

I have a quick question, How do redefining an operator and overloading an operator differ? In the context of user defined operators?

Post Information

  • Total Posts in this topic: 6 posts
  • Moderator: Tutorial Writers
  • Users browsing this forum: No registered users and 1 guest
  • You cannot post new topics in this forum
  • You cannot reply to topics in this forum
  • You cannot edit your posts in this forum
  • You cannot delete your posts in this forum
  • You cannot post attachments in this forum
 
 

© 1998-2014. Ozzu® is a registered trademark of Unmelted, LLC.