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:

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:

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:

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.

[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.
Since we cannot modify class 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 with string 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.


CS 202 Fall 2013: Notes for Tuesday, October 8, 2013 / Updated: 8 Oct 2013 / Glenn G. Chappell