| |
Operator Overloading in C++ by Ben Watson, 8/14/2002
Definition
This can be a weird subject for some, especially those with a strong Java background, or another language that doesn't support this feature. It can be confusing even for excellent programmers. But it is a strong feature of C++ that, if mastered, can yield some increased productivity in programming.
We all know that an operator can be used in mathematical expressions:
int z=x+y;
float g=3.14*g;
|
Now wouldn't it be nice to use operators on our own objects to do what we want? For example, a string class could use + to concatenate, or a Throttle class could use the ++ and -- operators to increase or decrease throttle position. The operators can be programmed to do whatever we want them to.
However, some words of caution. Operator overloading provides NO additional functionality to your code. It just compiles to normal function calls. It's even written out like normal function calls. It is mainly for aesthetics. There is, however, one extremely useful set of operators to overload that makes life much easier: the streaming operators, which I will cover at the end.
Second, you should NOT use operator overloading for unobvious relationships. Using + to concatenate two strings intuitively makes sense to most programmers, so it's easy to use it like that. But how would you define string1*string2? or string1^string2? It isn't very clear what that means. So use caution when considering adding operators to your objects.
Sample Object
For my sample object, I'm going to implement a matrix. This won't be a full-scale implementation of every imaginable matrix operation, but it should be enough to cover the basics of operator overloading, and maybe whet your appetite to complete the implementation for other operations (dot product, inverse, determinant, etc.).
In order to completely encapsulate a matrix within a class, we actually need two classes: Row and Matrix.
So let's start with Row:
template<class T>
class Row {
public:
Row(int cols=0):row(NULL) {SetRowSize(cols);}
~Row() {SetRowSize(0); }
Row(const Row &r):row(NULL) {
SetRowSize(r.numCols);
for (int i=0;i<numCols;i++)
row[i]=r.row[i];
}
void SetRowSize(int n) {
if(row) delete[] row;
if (n>0) {
row=new T[n];
memset(row,0,sizeof(T)*n/sizeof(char));
}
else row=NULL;
numCols=n;
}
int size() { return numCols;}
private:
int numCols;
T* row;
};
|
Let's look at this before continuing on. Notice that I'm making it a template class. This is so you can have a matrix of all the usual numerical types, as well as any type you want to define yourself. The only requirement for the type is that it must have the +, -, and * operators defined on it. We'll get into how to do that. If you don't understand templates, you can think of all of the T's as ints for now.
SetRowSize() deletes any old data, and allocates space for new data, unless we set the number of columns to 0, in which case it merely deletes the data. This lets us use this function for construction, destruction, and dynamic modification in one method. Nifty, eh? The call to memset() just zeroes out the array after figuring out how many bytes the row uses and dividing this by the size of character, because memset() works in terms of chars.
I also defined a copy constructor, which will come in handy quite a bit, as we'll see later on when we copy matrices.
Overloading [] OK, let's overload our first operator: []
Yes, that's one operator. The array-access operator. It makes perfect sense here, because we have a linear array of objects we would like to access. Let's add this definition to our Row class:
T& operator[](int column) {
assert(column<numCols);
return row[column];
}
|
The arguments to our brackets are going to be integers specifying the index of the item we want, so that will be the function's arguments. Notice the syntax: [ReturnType] operator[Op]([argument list]). We do an assertion to make sure we're accessing memory within the array's bounds. If all is OK, we return a reference to the object. Why a reference instead of a value? It won't make much of a difference in a case like this:
Row<int> r(1);
int a=r[0];
|
a will get the value of r[0] whether a reference or a value is returned. However, if we return a reference, we can then change the value in the row from outside the class, using the [] accessor operator, like so:
Row<float> r(1);
r[0]=3.142;
float pi=r[0];
|
Very cool, isn't it.
Overloading =
The only other operator we need to overload is assignment (=). When overloading assignment, we must keep in mind that the object we're assigning to must already exist, and it is that object's operator= method which will be called.
Row& operator=(const Row& r) {
SetRowSize(r.numCols);
for (int i=0;i<numCols;i++)
row[i]=r.row[i];
return *this;
}
|
Again we return a reference, but this time it's a reference to itself. First we set the size of the current row equal to that of the source row, then we copy its values. There is an important note here. Notice that I'm using [] on the primitive T array itself--NOT the overloaded []s of Row. Remember that Row's [] returns a reference, thus if we had written row[i]=r[i], we would get a row that references the exact same data in memory, so that when we changed one the other would change--this isn't what we want at all, so we need to access the raw data in the Row class.
Now we can write code like this:
Row<double> r1(5);
Row<double> r2;
Row<double> r3(2);
r2=r1;
r3=r1;
|
Matrices are Made of Many Rows
Now that we have a working Row, we can combine rows into a matrix. Let's start with this basic definition:
template<class T>
class Matrix {
public:
Matrix(int rows=0, int cols=0): matrix(NULL) {
SetSize(rows,cols);
}
Matrix(const Matrix& m): matrix(NULL) {
SetSize(m.numRows,m.numCols);
for (int r=0;r<numRows;r++)
matrix[r]=Row<T>(m.matrix[r]);
}
void SetSize(int rows, int cols) {
if (rows) delete[]matrix;
if (cols > 0 && rows >0) {
matrix=new Row<T>[rows];
for (int i=0;i<rows;i++)
matrix[i].SetRowSize(cols);
}
else
rows=NULL;
numCols=cols;numRows=rows;
}
int GetCols() { return numCols;}
int GetRows() { return numRows;}
private:
int numCols, numRows;
Row<T>* matrix;
};
|
This follows very closely the basic form of the Row class. The only item of interest is when we declare and allocate a matrix: we must specify the type, T, after the class name.
First let's implement the same operators we did on the Row class:
Row<T>& operator[](int index) {
assert(index<numRows);
return matrix[index];
}
Matrix& operator=(const Matrix& m) {
SetSize(m.numRows,m.numCols);
for (int r=0;r<numRows;r++)
matrix[r]=Row(m.matrix[r]);
return *this;
}
|
The most important part of this code is the return type of operator[]. It returns a reference to a Row of type T. This little fact allows us to use the Matrix class like this:
Matrix<int> a(2,2);
a[0][0]=2;
a[0][1]=4;
a[1][0]=8;
a[1][1]=16;
|
That is, we can refer to Matrix objects now with exactly the same notation as primitive 2-D arrays in C++: array[row][column]. Our operator overloading is faking it well enough to keep a consistent interface with analogous structures, but add much more functionality and safety. Isn't this cool?
The = operator works the same way as in Row. It sets the size of the current Matrix to that of the source, and then copies all of the objects to the current Matrix. Now we can do the following:
Matrix<__int64> m(1000,1000);
Matrix<__int64> n=m;
|
...and we have two very large matrices of 64-bit integers.
Overloading +
Let's do some more interesting things with these matrices now. There are a number of mathematical operations that can be performed on a matrix, the simplest perhaps is addition. Addition of matrices requires that they both have the same dimensions. The resulting matrix is made by simply adding each number in the same position in each matrix and putting the answer in the same position as the two operands.
[1 0] [4 3] [5 3]
[2 1] + [-1 0] = [1 1]
|
Since addition creates a new matrix, we don't want to return a reference, but an actual matrix object. Here's what the code looks like:
const Matrix operator+( const Matrix& m) {
assert(numCols==m.numCols && numRows==m.numRows);
Matrix theMatrix(numRows,numCols);
for (int r=0;r<numRows;r++)
for (int c=0;c<numCols;c++)
theMatrix[r][c]=matrix[r][c]+m.matrix[r][c];
return theMatrix;
}
|
This adds the current matrix to the matrix in argument m. We first assure that the dimensions are equivalent, then create a new matrix with the same dimensions as the sources. It is then a simple matter of adding the two sources, and returning the new matrix. Notice that we perform the actual math on the types that make up each row.
Matrix<float> a(2,2);
Matrix<float> b(2,2);
Matrix<float> c(2,3);
Matrix<float> d=a+b;
Matrix<float> e=a+c;
|
It is just as easy to define subtraction:
const Matrix operator-( const Matrix& m) {
assert(numCols==m.numCols && numRows==m.numRows);
Matrix theMatrix(numRows,numCols);
for (int r=0;r<numRows;r++)
for (int c=0;c<numCols;c++)
theMatrix[r][c]=matrix[r][c]-m.matrix[r][c];
return theMatrix;
}
|
Overloading += and -=
+= and -= are operators that both add and change the current object, so the code to describe it is a combination of +/- and =. We'll return a reference again because we don't want to create a new object, but just modify the existing one, which called the function. We'll just add whatever is currently in it to the other matrix, and return a reference to itself:
Matrix& operator+=(const Matrix& m) {
assert(numCols==m.numCols && numRows==m.numRows);
for (int r=0;r<numRows;r++)
for (int c=0;c<numCols;c++)
matrix[r][c]+=m.matrix[r][c];
return *this;
}
Matrix& operator-=( const Matrix& m) {
assert(numCols==m.numCols && numRows==m.numRows);
for (int r=0;r<numRows;r++)
for (int c=0;c<numCols;c++)
matrix[r][c]-=m.matrix[r][c];
return *this;
}
|
We can now expand our repertoire to include the following possibilities:
Matrix<int> a(2,1);
Matrix<int> b(2,1);
a+=b;
a-=b;
|
Scaling: Overloading *
Another useful operation we can perform on matrices is scaling. This just multiples every element in the matrix by a constant.
[1 2] [2 4]
[2 4] * 2 = [4 8]
|
This operation returns a new matrix so we will return by value, not reference. The code should be trivial to read by now:
const Matrix operator*(const float s) {
Matrix theMatrix(numRows,numCols);
for (int r=0;r<numRows;r++)
for (int c=0;c<numCols;c++)
theMatrix[r][c]=matrix[r][c]*s;
return theMatrix;
}
|
We use a float as the scalar, and multiply it by every value in the source matrix, and return a new matrix. It is left up to the reader to implement *=. (/ and /= could also be implemented as inverses of scaling, but since scaling allows a float, this is mostly redundant.)
Matrix Multiplication - Overloading * again
We can actually overload the same operator more than once if we would like. As long as the function's signature (return type, name, and arguments) is different, we can define as many as we want. The * operator would be a likely candidate for implementing matrix multiplication as well as scaling. Both imply multiplication of some sort, so it should make sense.
Matrix multiplication has a requirement: the number of columns in the first matrix must be equal to the number of rows in the second. Matrix multiplication is NOT commutative. I won't explain how to do matrix multiplcation--it's easy enough to look up this topic on-line or in a math textbook. Or you can deduce the "by-hand" algorithm from the code.
const Matrix operator*(Matrix& m) {
assert(numCols==m.numRows);
Matrix theMatrix(numRows,m.numCols);
for (int r=0;r<numRows;r++) {
for (int c=0;c<m.numCols;c++) {
for (int i=0;i<numCols;i++) {
theMatrix[r][c]+=matrix[r][i]*m[i][c];
}
}
}
return theMatrix;
}
|
Overloading << and >> There are only two more important operators that I will cover here. These are perhaps the operators that should be implemented for each and every class you create. The streaming operators << and >> allow your object to be saved and restored from any stream, be it console, network, or file.
There is a slight additional challenge with these operators because we must allow the stream access to our object's private data. Therefore, these functions must be declared as friends inside the Matrix class.
Let's first look at outputting to a stream:
friend ostream& operator<<(ostream& os,const Matrix& m) {
os << m.numRows<<" "<<m.numCols<<" "<<endl;
for (int r=0;r<m.numRows;r++) {
for (int c=0;c<m.numCols;c++) {
os << m.matrix[r][c] << " ";
}
os <<endl;
}
return os;
}
|
We include this function in our Matrix class and declare it to be friend. It does not have to be defined in our Matrix class, but that's where I left it. First we output the number of rows and columns we have, making sure to separate this data by a space to keep it intact. Then we just run through a loop, outputting each row on its own line. A very important thing to note about overloading streaming operators is that we always return a reference to the same stream we passed in. Why? Because that's what allows us to stack output and input:
Matrix<float> a(2,2);
cout <<"Matrix a:"<<endl<< a<<endl;
ofstream ofile("output.dat");
ofile << a << endl<<"End of File";
|
It is a quite similar technique to read in values from a stream:
friend istream& operator>>(istream& is, Matrix& m) {
int rows,cols;
is >> rows >> cols;
m.SetSize(rows,cols);
for (int r=0;r<rows;r++)
for (int c=0;c<cols;c++)
is >> m[r][c];
return is;
}
|
Here we declare some local variables to hold our matrix dimensions, which we then pass to the referenced matrix object. Then it's just a matter of reading in the next number and putting it in the appropriate location. We then return a reference to the stream in case the calling function wanted to continue getting data from it in the same command.
Putting it all together
To demonstrate the Matrix class and its overloaded operators, I've written a sample main() as well as some helper functions that will run the class through its paces:
void init_matrix(Matrix<int>& m) {
for (int r=0;r<m.GetRows();r++)
for (int c=0;c<m.GetCols();c++)
m[r][c]=rand()%5+1;
}
void init_matrix(Matrix<__int64>& m) {
for (int r=0;r<m.GetRows();r++)
for (int c=0;c<m.GetCols();c++)
m[r][c]=rand()%100000+1;
}
void init_matrix(Matrix<float>& m, int precision) {
for (int r=0;r<m.GetRows();r++)
for (int c=0;c<m.GetCols();c++) {
float dec=float(rand()%precision)/precision;
m[r][c]=float(rand()%5)+1.0+dec;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
srand((unsigned)time(NULL));
Matrix<int> a(5,5);
init_matrix(a);
a[0][0]=-13;
a[1][1]=-13;
cout << "Writing to file from Matrix a:" <<endl;
cout << a<<endl;
ofstream of;
of.open("test.txt");
of << a<<endl;
of.close();
ifstream iff("test.txt");
if (!iff) {
cout << "Error opening file"<<endl;
return 1;
}
Matrix<int> b;
cout << "Reading from file into Matrix b:"<<endl;
iff >> b;
iff.close();
cout <<endl;
cout << b<<endl;
cout <<"Press any key to continue..."<<endl;
getchar();
Matrix<float> c(3,2);
init_matrix(c,100);
cout <<"Matrix c:"<<endl<< c<<endl;
Matrix<float> d(3,2);
init_matrix(d,100);
cout << "Matrix d:"<<endl<<d<<endl;
cout << "c+d:"<<endl<<c+d<<endl;
cout <<"Press any key to continue..."<<endl;
getchar();
Matrix<float> e(10,10);
init_matrix(e,1);
float scalar=-1.5;
cout << "Matrix e:" << endl<<e<<endl;
cout << "Scalar: "<<scalar<<endl;
cout << "e * scalar:"<<endl<<e*scalar<<endl;
cout <<"Press any key to continue..."<<endl;
getchar();
Matrix<__int64> f(3,5);
Matrix<__int64> g(5,6);
init_matrix(f);
init_matrix(g);
cout <<"Matrix f:"<<endl<<f<<endl;
cout <<"Matrix g:"<<endl<<g<<endl;
cout <<"f*g:"<<endl<<f*g<<endl;
cout <<"Press any key to continue..."<<endl;
getchar();
return 0;
}
|
Conclusion
Operator overloading can be a powerful programming tool when it increases usability and understandability of a class. In this case, it's much nicer to write matrix[0][4] rather than matrix->GetRow(0)->GetCol(4). However, it must always be remembered that overloading serves to make programming easier--it doesn't benefit the end user at all. Overloading an operator must make sense, as I hope all of the decisions I made in this class make sense. If it's appropriate, go ahead and use it. If it will make people wonder what you meant, than it's probably something to stay away from.
Also, because this is a template, you could possibly have a matrix of any object, including, for example strings. This will work perfectly in some situations, but not in others. Basically, if + is defined on strings, then you can do matrix addition on a string matrix. However, * is usually not defined for strings, so a statement with that would refuse to compile.
Matrix<string> s(2,7);
Matrix<string> t(2,7);
Matrix<string> u=s+t;
Matrix<string> v(7,5);
Matrix<string> w=s*v;
|
I did not make use of the -- and ++ unary operators in this tutorial because they don't always make sense when used on a matrix. With these operators it is important to know that since they can be both prefix and postfix, each version has a slightly different function signature. With the understanding you have gained in this tutorial, you should be able to look up how to implement these operators without trouble.
Hope this helps somebody understand this subject better! Happy coding!
|
|