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:
- 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 theoperator<<
definition. Don’t want to force our users to use new lines.
Consider:
- 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<<
andoperator>>
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.
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
- Overloading operators gives us a convenient syntax.
- Classes allow us to enforce invariants, good ADT design.
- 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:
An rvalue is a temporary values, it will be destroyed “soon”. For example:
Another example:
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:
An exception: We can bind rvalues to const lvalue references (No complaints of changing anything!)
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)
- 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:
Why is this useful? We’ll see shortly. Finally - note type and value categories are independent properties
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.
I want the following client code:
First, ctor:
Next:
- 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:
- Recursively calling itself with
new Node{*(other.next)}
. NowNode p{n};
will perform a deep copy withour custom copy ctor: - This is a deep copy, as opposed to a shallow copy.