Last Time: ADT design, explicit, MIL. This Time: MIL finished, operator overloading

Rational(int num, int denom): num{num}, denom{denom} {}
 
vs
 
Rational(int num, int denom){
	this->num = num;
	this->denom = denom;
}

In MIL, we give it the value immediately whereas in setting things in the constructor body, you have to default construct an object field. It takes time because it will have a method call, then you immediately overwrite it in the body.

  • Using the MIL is considered better style than assigning the fields in the ctor body.
  • Always use it.

There are cases where using the MIL is necessary:

1) const fields
class Student {
	const int id; // field id is const and we try to change it in the ctor, then it violate the const of that field.
public:
	Student(int id) {
		this->id = id; // does not compile, have to use MIL
	}
}
 
2) Reference fields
class Student {
	University& myUni; // a reference to myUni
public:
	Student(University& uni){
		this->myUni = uni; // does not compile, reference always must have a value! References must be initialize.
	}
}
// Uni doesn't have a value once it enters the ctor body.
 
3) Object fields without default ctors
class A {
public:
	A(int x){}
};
 
class B {
	A myA;
public:
	B() {
		myA = A{5} // doesn't work
	}
};
// myA is not initialized in the MIL of B(), hence it tries to default construct. Goes to A and look for a default constructor for A in class A. There is none. So it does not compile.
 
4) If the superclass does not have a default ctor
class A{
	int x;
public: 
	A(int x):x{x}{}
};
 
class B: public A{ // inherits from A
	int y;
public:
	B(int x, int y){
		this->x = x;
		this->y = y;
	}
};
  1. field id is const, need to use MIL
  2. References must always be initialized (have a number).
  3. Tries to default compile A, but it’s not in class B. So does not work.
  4. Stuck on step2: Since B inherits from A, it will want to store x too in itself, so it will want to initialize x but it’s not in the MIL of B. It will try to default construct it from inside class A. But there is no default constructor because we provided a constructor. Hence, it won’t compile.
    • What if we gave A a default ctor?
class A {
	int x;
public:
	A(): x{0}{}
	A(int x): x{x}{}
};
 
class B: public A{ // B inherits from A
	int y;
public: 
	B(int x, int y){
		this->x = x; // does not compile, x is private.
		this->y = y;
	}
};

Breaks down for a different reason. Step 2: Didn’t specify how to initialize in superclass, it goes to A and default constructs. Attempts to initialize x to 0. Step 3: fields are initialized, integer y is a primitive field, it will be left with a garbage value. Step 4: constructor body runs. Cannot set the value of this→x = x because x is a private field. Cannot set private fields in the subclass, because they are not accessible. Only public and protected fields.

Correct way: to initialize B with MIL

class B: public A{
	int y;
public:
	B(int x, int y): A{x}, y{y} {}
}; 
// This works even if A does not have a default ctor.

We specified that we wanted to initialize the A portion of this object using the value of x. When we are creating our B object, we need to initialize the A portion, we call the constructor taking in one argument, calls the constructor in A. All goes well. BUT since we don’t have default ctor for A, and you call when constructing in B A{} without arguments, it won’t compile.

The 4 Steps:

  1. Space is allocated (stack or heap).
  2. Call the superclass ctor.
  3. Initizalize fields via MIL.
  4. Run the ctor body.

Now let’s consider support Rational Operations:

Overloading

To support this functionality, we need overloading.

Example
bool negate(bool) {
	return !b;
}
 
int negate(int x){
	return -x;
}

Note

Cannot overload based solely on return type.

To perform an operator overload we define a function witht the name operator we define a function with the name operator concatenated with the operator symbol.

operator +, operator>>, operator!, operator==

The number of arguments, must match the arity of the operator.

Example
+: binary operator - 2 args
!: unary operator - 1 args
To support cin>>r>>q; where r, q are Rationals. Define the following operator overload:
istream& operator>>(istream& in, Rational& r){
	in >> r.num;
	in >> r.denom;
	return in;
}
  • cin is passed to istream& in, r is passed to Rational& r .
  • Why is istream passed by reference &
    • because copying is disallowed for istreams
  • The Rational is passed by reference because we want changes to be reflected outside this function.
  • Why return in?
    • We return in to allow for chaining.
    • cin >> r >> q
      • cin >> r is evaluated first.
      • If we return in, it simplifies to cin >> q.

Problem

r.num and r.denom are private fields, cannot be accessed in the operator.

Solution 1: Provide accessor methods

  • Provide methods getNum and getDenom which return references to the num and denom fields.
  • Sometimes paired with mutator methods setNum and setDenom..
    • (Could enforce invariants like denom != 0) with these methods.

These are sometimes called getters/setters

Solution 2: Declare operator >> as a Friend (C++)

class Rational {
	int num, denom;
	...
	friend istream& operator>>(istream& in, Rational& r);
};
// this function can access any private fields + methods of Rational

Now, support p = q + r adding two rationals together. Define operator+ for two Rationals:

Rational operator(const Rational& lhs, const Rational& rhs){
	return Rational{lhs.num*rhs.denom+lhs.denom*rhs.num, lhs.denom*rhs.denom};
}
// This would need to be a friend as well. (same reason)

Take in arguments via constant reference:

  • Constant - don’t want lhs or rhs to change.
  • Reference - quick, no copying.

Declaring all these overloads as friends is a pain!

Alternative: Define operator overloads as methods in Rational.

class Rational {
	int num, denom;
public:
	...
	Rational operator+(const Rational& lhs, const Rational& rhs){
		return Rational{num*rhs.denom + denom*rhs.num, denom*rhs.denom};
	}
};
// Now, defined in the class, no friend is necessary.
  • this takes the place of the lhs
    • r+q == r.operator+(q);

Note

operator<< and operator>> are usually defined as standalone functions. Because cin/cout appear on the lhs.

What if we want r+5?
class Rational{
	...
	Rational operator+(int rhs){
		...
	}
 
};
What about 5+r?

Order of args matters. We want an integer on the lhs, so we need a standalone function here:

Rational operator+(int lhs, const Rational& rhs){
	return rhs + lhs; // looking for a rational on the lhs and integer on the rhs, which is operator+ (calls this function)
}
What about p = q + r?

Setting one Rational to another?

  • Compiler provides a copy assignment operator for you.
  • Can also write our own copy assignment operator:
class Rational{
	...
public:
	Rational& operator=(const Rational& rhs){
		num = rhs.num;
		denom = rhs.denom;
		return *this;
	}
};
  • Why is the return type Rational&, why do we return * this? We can also chain operator= a=b=c=d=e; Evaluates right to left, returns the value that was set. d=e executes first. Returns reference to d. Simplifies to a=b=c=d.

Next: CS 247 Lecture 3 Links to this page CS247 - Software Engineering Principles