Thursday, 16 February 2012

Operator Overloading in C++ Part 1

We introduced the concept of operator overloading today in the first year lecture / lab session and there was quite a lot of confusion, so I've decided to do a more in depth article about it here.

There is a wealth of knowledge online about operator overloading and a good starting point is this article as well as the venerable c++ faq, however I'm going to use the excellent book by Martin Reddy "API Design for C++" as he discusses some interesting differences between the ways we can implement Operator Overloading.

All of the code mentioned in this article can be downloaded from here

So what is Operator Overloading
The main reason to overload operators in your own class design is to make them behave like the built in types (intrinsic types). It can also make things more intuitive when writing our own classes, especially if they are representing something mathematical. For example imagine the following class

We have three attributes of type float in the private section, and (assuming a constructor has been written )  we could write the following code
Vec3 a,b,c;
add(add(mul(a,b), mul(c,d)), mul(a,c))
But that is almost unreadable and the actual mathematic being implemented is $$ a*b+c*d+a*c $$ which is much more readable, and when trying to implement a mathematical function from a paper much easier to check when debugging.

So we can use operator overloading to make our classes work like mathematical data types even when they are not.

However, it is important to only overload operators which seem natural to do so and follow the semantic you would expect. For example it is natural to overload the plus + operator to meen addition or for string based objects concatenation.

Vec3 Class
For the purpose of this discussion I will be using a simple floating point Vec3 class which represents a three tuple vector and will be used to perform mathematical calculations and comparison operations. The basic header file we will be using is
#ifndef VEC3_H_
#define VEC3_H_

class Vec3
{
  public :
  Vec3(
        float _x=0.0f,
        float _y=0.0f,
        float _z=0.0
      ) :
          m_x(_x),
          m_y(_y),
          m_z(_z){;}

  private :
    /// @brief the x element
    float m_x;
    /// @brief the y element
    float m_y;
    /// @brief the z element
    float m_z;
};

#endif
This will allow us to create simple Vec3 classes like this
#include "Vec3.h"

int main()
{
  Vec3 a(1,2,3);
  Vec3 b;
  // use the compilers built in assignment operator
  b=a;

}
In the above example we construct a Vec3 a and assign it some values, and be will be constructed using the default parameter values 0,0,0. By default the compiler will implement a simple assignment operator for us and a==b. (don't worry we will create our own soon).

Free Operators vs Member Operators
Reddy defines two different types of operator, "Free Operators" and "Member Operators" Member operators are members of the class, and as such have full access to the private data areas of the class, there are some operators that can only be implemented using Member Operators ( = [] -> ->* () (T) new / delete) however all the other operators can be implemented in both ways. In this example I'm going to implement both type using two different project files (which can be downloaded from the link at the top of the page).

Free Operators are not part of the class, and are declared as functions separate to the class, this means that the function doesn't have access to the class private data area so we must write some form of accesors for the attributes for it to work, however this does have the advantage of allow us to reduce the coupling of the Vec3 class to our other implementation.

The other advantage of Free Operators is that they give us better symmetry and allow us to do something like 2*V and V*2 in the same code.

Insertion Operator
The first operator I'm going to implement is the << insertion operator, this will allow us to use std::cout to print the contents of our class, this is perhaps not the best one to start with as it actually add extra complication to what we are doing as we need to send data to the std::ostream class and it will need to know about the internal structure of our Vec3 class. The easiest way of doing this, and in most texts the way shown, is by using the friend prefix this indicates that the method is a friend of the class and it can access the private data. This does however break encapsulation and promotes many coding arguments.

The following code will implement the << operator as a member operator in is placed in the Vec3.h
friend std::ostream& operator<<(std::ostream& _output, const Vec3& _v); 
We can now write the implementation code in the Vec3.cpp file
std::ostream& operator<<(
                         std::ostream& _output,
                         const Vec3 & _v
                        )
{
  return _output<<"["<<_v.m_x<<","<<_v.m_y<<","<<_v.m_z<<"]";
}
This will format the string and return it to the ostream object, we can use the code in the following way
#include "Vec3.h"
#include <iostream>

int main()
{
  Vec3 a(1,2,3);
  Vec3 b;
  Vec3 c(2,3,4);

  std::cout<<"a "<<a<<" b "<<b<<" c "<<c<<"\n";
}
And will give the output
a [1,2,3] b [0,0,0] c [2,3,4]
To generate the free operator version we actually have to write a lot more code in the Vec3 Class. As I mentioned earlier the free operator version is defined outside of the class definition, usually I still prefer to keep the definition and the code in the same .h and .cpp files however this is not mandatory anymore as the free operators are not part of the class. The following code shows the prototype of the free operator
std::ostream& operator<<(std::ostream& _output, const Vec3& _v);
Now as this is no longer a friend to our Vec3 class we need to implement a method to gather the data we require and return it to the free operator in this case I'm going to implement the member function getString() as follows
/// @brief a method to get the data as a string
std::string getString() const;
And the code to format the string and return it
#include <sstream>

std::string Vec3::getString() const
{
  std::stringstream string;
  string<<"["<<m_x<<","<<m_y<<","<<m_z<<"]";
  return string.str();
}
Here we construct a stringstream object and pass the formatted data to it. This is then converted to a string and returned from the method. Finally the free operator can access the data from the class as follows
std::ostream& operator<<(
                         std::ostream& _output,
                         const Vec3 &_v
                        )
{
  return _output<<_v.getString();
}
The client program to use the operator will be exactly the same.

2 comments:

  1. https://youtu.be/Vk1Yj1KAN2c
    check this video to understand the concept of operator overloading

    ReplyDelete