CS 202 Fall 2013 > Notes for Tuesday, October 8, 2013 |
CS 202 Fall 2013
Notes for Tuesday, October 8, 2013
Operator Overloading (cont’d) [14.5]
Operator Overloading Using Member Functions
So far, we have writen operator functions as global functions. For many operators, we can also use member functions. When we do this:
- The class the function is a member of, is the type of its first operand—which is no longer passed as a parameter.
- If a global function would take the first operand
by reference-to-const,
then the member function should be
const
. If a global function would take the first operand by reference, then the member function should not beconst
.
For example,
here is a +=
operator as a global function
[C++]
Foo & operator+=(Foo & a, const Foo & b) { ... return a; }
Suppose we prefer to write this as a member function.
Following the above rules,
it would be a member of class Foo
,
and it would not be const
.
But what do we return?
Inside a (non-static) member function,
this
is a pointer to the current object.
So the current object itself is *this
;
that is what we return.
Here is the above +=
operator,
rewritten as a member.
[C++]
class Foo { ... Foo & operator+=(const Foo & b) { ... return *this; } ... };
Similarly, here is a binary +
operator as a global
function.
[C++]
Foo operator+(const Foo & a, const Foo & b) { Foo copy_a(a); copy_a += b; return copy_a; }
And here it is as a member.
[C++]
class Foo { ... Foo operator+(const Foo & b) const { Foo copy_this(*this); copy_this += b; return *this; } ... };
Which to choose: global or member? Here is a good rule to remember:
- Implement an operator using a global function, unless you have a good reason not to.
The Assignment Operator
Copy Assignment: When to Write It
Here is one such good reason:
the C++ Standard requires
the assignment operator (=
)
to be a member function.
Note, however, that we
usually do not need to write an assignment operator,
as the compiler writes one for us.
Specifically, the copy assignment operator,
which takes the same type as that being assigned to
(think “Foo = Foo
”)
is written for us if we do not declare it,
just like the copy constructor and the destructor.
The compiler-generated version does a copy assignment of each
member variable.
These three member functions (destructor, copy constructor, copy assignment operator) are called the big three. We usually do not need to write any of these three member functions. But if we write one of them, then we should probably write the rest, too. This is called the Law of the Big Three.
Law of the Big Three. If you declare a destructor, copy constructor, or copy assignment operator for a class, then you should probably declare all three.
Again, for most classes, the compiler-generated versions of the big three are fine. We usually write them for classes that manage some kind of resource (like dynamically allocated memory). Here
Copy Assignment: How to Write It
Suppose we have decided to write the big three. If we have written a copy constructor, then we already have code that tells how to copy. We would like to use this in the copy assignment operator.
But there is a problem. The copy constructor always creates a new object. Copy assignment always sets the value of an existing object. But a nice trick involving swapping lets us use the copy constructor anyway.
Here is how we write copy assignment using the swap trick:
- Create a copy of the right-hand side operand of the assignment operator, using the copy constructor.
- Swap the values of each member variable with the corresponding member variable of the copy.
- Return the current object (
*this
).
The current object will have the right value. What happens to its old value? The copy gets that value, and its destructor cleans it up.
For example, here is a class that manages dynamically allocated memory. The destructor and copy assignment operator are already written.
[C++]
class HasArray { public: // Constructor from size HasArray(int size) { _size = size; _arr = new int[_size]; } // Destructor ~HasArray() { delete [] _arr; } // Copy constructor HasArray(const HasArray & other) { _size = other._size; _arr = new int[_size]; for (int i = 0; i < _size; ++i) _arr[i] = other._arr[i]; } // Copy assignment HasArray & operator=(const HasArray & rhs); private: int _size; int * _arr; };
Here is how we would write the copy assignment operator, using the swap trick.
[C++]
#include <algorithm> using std::swap; HasArray & HasArray::operator=(const HasArray & rhs) { // Make a copy of right-hand side HasArray copy_rhs(rhs); // Swap each member variable swap(_size, copy_rhs._size); swap(_arr, copy_rhs._arr); // Return current object return *this; // Note: The destructor of copy_rhs cleans up our old value }
Now we can add one more row to the table from last time.
Fundamental Operation | Others to Make from It |
---|---|
Copy constructor | Copy assignment (= ) operator |
Stream Operators
Stream Insertion (<<
)
When we do “cout << 3.45;
”
we are using a stream insertion operator.
This takes two parameters:
one of type ostream
,
and one of the type we want to print.
The operator returns the stream it is given. This is why we can chain multiple stream insertions.
Since we cannot modify class[C++]
cout << a << b; // The << operator is left-associative, so the above // is the same as the following. (cout << a) << b; // And the value of (cout << a) is cout. // So b gets printed to cout as well.
ostream
,
a new stream insertion operator must be a global function.
It modifies the stream (by printing to it),
so it takes its first parameter by reference,
and it returns by reference.
[C++]
#include <iostream> using std::ostream; ostream & operator<<(ostream & outs, const Foo & printme) { ... return outs; }
Inside the function, we can use the stream
(above, outs
)
just like always.
Perhaps we can print some of the members of printme
with it.
[C++]
{ outs << printme._x << " " << printme._y; return outs; }
What we must not do is
“outs << printme
”.
That is what the function we are writing is for.
Stream Extraction (>>
)
When we do “cin >> x;
”
we are using a stream extraction operator.
This takes two parameters:
one of type istream
,
and one of the type we want to input.
A stream extraction operator is written in much the same way as a stream insertion operator. Note that we modify the second operator; it is passed by reference.
[C++]
#include <iostream> using std::istream; istream & operator>>(istream & ins, Foo & inputme) { ins >> inputme._x >> inputme._y; return ins; }
Aggregation [14.7]
Aggregation, also called composition means making an object a member variable of another object. We have already done this withstring
objects;
we can do it with our own types, too.
When an object is created, all its member variables are constructed just before the constructor is executed. We can determine what parameters are passed to the constructors of member variables using the following syntax.
[C++]
class Foo { public: Foo(const string & s) :_short(s.substr(0,3)), // Ctor calls for _short & _long _long(s) {} private: string _short; string _long; };
Whenever we might want to write
“_m = ...
” in a constructor
(where _m
is a member variable),
we can use the above syntax.
So this:
[C++]
Foo(const string & s) { _short = s.substr(0, 3); _long = s; }
can become this:
[C++]
Foo(const string & name, int age=0) :_short(s.substr(0,3)), _long(s) {}
So it is common for a constructor to have an empty function body
(“{ }
”).
An important point:
Member variables are constructed in the order they are declared.
In the above class, member _short
is declared before member _long
,
so it is always constructed first.
That means the code below does not quite do what you might think.
[C++]
Foo(const string & s) :_long(s), // _short is constructed first! _short(s.substr(0,3)) {}
And the code below has a bug. Do you see it?
[C++]
Foo(const string & s) :_long(s), _short(_long.substr(0,3)) // BUG!!! {}
For today’s lab work, see the 10/8 Challenge.