CS247 Lecture 3

Last time: CS247 Lecture 2 MIL, Operator overloading This Time: Operator overloading finished. Value categories. Linked List Abstract Data Type.

Implementation as a method for operators

There are some operator overloads that MUST be defined as methods in the class. These are:

  • operator=
  • operator[]
  • operator->
  • operator()
  • operator T where T is a type.

Supporting division of Rationals, e.g q/r. (Exercise)

Supporting cout << q << endl where q is a Rational:

class Rational{
	...
	friend ostream& operator<<(ostream& out, const Rational& r);
};
 
ostream& operator<<(ostream& out, const Rational& r){
	out << r.num << "/" << r.denom;
	return out; // to support chaining
}
  • We want an ostream on the lhs→standalone function, need access to q.num, q.denom, need to declare as a friend.
  • Usually we don not provide endl in the operator<< definition. Don’t want to force our users to use new lines.

Consider:

ofstream file{"file.txt"};
Rational r{5,7};
file << r << endl;
  • ofstream is an ostream, so here, we print out in the file. We pass file into out. Print out to the file r.num / r.denom.
  • Our operator<< and operator>> definitions also work for reading from and printing to files (and other types of streams).

Finally, Rational z{q};

or Rational z = q; or Rational z(q)

  • Running the copy constructor. (because z is declared for the first time)
  • Compiler provides a copy ctor that simply copies all the fields. We can also write our own.
class Rational{
	...
public:
	Rational(const Rational& other): num{other.num}, denom{other.denom}{}
};
Rational q{3, 4}
Rational z{q}; // copy ctor
Rational r{2, 3};
Rational p{1, 2};
r = p; // assignment operator, it already exists

Having written all of these operator overloads our Rational Abstract Data Type is easy to use from a client perspective. Exercise: Preventing denom = 0. Multiplication and substractoin arithmetic operators.

Takeaways

  1. Overloading operators gives us a convenient syntax.
  2. Classes allow us to enforce invariants, good ADT design.
  3. Friends can be used (sparingly) to give access to private fields.

As they say in C++ You should never have too many friends. Which shouldn’t be a problem for waterloo students…

Value Category

Every expression in C++ has both a type and a value category. We’ll discuss 2 categories: lvalues and rvalues.

An lvalue is any expression that you can take the address of. For example:

int x = 5;
f(x); // this expression 'x': we can take its address -> it is an lvalue

An rvalue is a temporary values, it will be destroyed “soon”. For example:

f(5); // 5 is an rvalue. We cannot take the address of 5.

Another example:

string f(){
	return "Hello World";
}
 
string s = f(); // This expression is an rvalue. The returned result of f only exists until the end of the line.

We cannot run &f() - the string isn’t stored anywhere permanently, just put in a temporrary until it is saved into s.

Note

The references we have seen so far are lvalue references. These can only bind to expressions that are lvalues.

For example:

int x = 5;
int& y = x; // check
 
int& z = 5; // 5 is an rvalue, z is a reference. Does not compile.

An exception: We can bind rvalues to const lvalue references (No complaints of changing anything!)

f(int& x){ // does not compile
	...
}
g(const int& x)}{ // compiles
	...
}
 
f(5); // not allowed
g(5); //although 5 is rvalue, we are not going to change x in g() because of const. 
 

This is allowed, we won’t modify x, the compiler creates a temporary memory location to store the 5 in.

We can create rvalue references

Extend the lifetime of the rvalue to the lifetime of the reference. (References to temporary values)

string f(){
	return "Hello World";
}
 
string&& s = f(); // string&& is an rvalue reference.
  • Can use the temporary value returned by f for as long as s exists.
  • Usually we just do string s = f();

Most commonly, used for overloading functions based on the value category of the expression:

void f(const string& s){
	cout << "1" << endl;
}
 
void f(string&& s){ // takes in rvalue reference to string
	cout << "2" << endl;
}
 
string s{"CS247"};
f(s); // lvalue, we get 1
f(string{"CS247"}); // creating temp string to store cs247 is an rvalue, we get 2

Why is this useful? We’ll see shortly. Finally - note type and value categories are independent properties

void f(string&& s){
	cout << s << endl; // in this expression, although s references an rvalue, we can take s's address -> s is an lvalue.
}

Linked List Abstract Data Type

A slightly more complicated ADT example. Specifically I want to leverage lvalue/rvalue knowledge for efficiency. We’ll focus on invariants and encapsulation later. Now, we want correctness and efficiency.

struct Node {
	string data;
	Node* next;
};

I want the following client code:

Node n{"a", new Node{"b", new Node{"c", nullptr}}};
Node p{n};
p.next->data = "z";
cout << p; // a, z, c
cout << n; // a, b, c

First, ctor:

Node::Node(const string& data, Node* next): data{data}, next{next}{}

Next:

Node p{n}; // This executes the copy ctor - provided by compiler. Copies each field.

  • But we just want p to be modified if we just use the compiler provided compiler!
  • Now, p.next->data = "z", modifies n as well! We have shared data. We’ll define our own copy ctor that recursively copies the data structure.

Custom copy ctor:

Node:: Node(const Node& other): data{other.data},next{other.next? new Node{*(other.next)}:nullptr}{}
  • Recursively calling itself with new Node{*(other.next)}. Now Node p{n}; will perform a deep copy withour custom copy ctor:
  • This is a deep copy, as opposed to a shallow copy.