CS247 Lecture 2
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;
}
};
- field id is const, need to use MIL
- References must always be initialized (have a number).
- Tries to default compile A, but it’s not in class B. So does not work.
- 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:
- Space is allocated (stack or heap).
- Call the superclass ctor.
- Initizalize fields via MIL.
- 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 toRational& 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
andr.denom
are private fields, cannot be accessed in the operator.
Solution 1: Provide accessor methods
- Provide methods
getNum
andgetDenom
which return references to the num and denom fields. - Sometimes paired with mutator methods
setNum
andsetDenom.
.- (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<<
andoperator>>
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 chainoperator=
a=b=c=d=e;
Evaluates right to left, returns the value that was set.d=e
executes first. Returns reference to d. Simplifies toa=b=c=d
.
Next: CS247 Lecture 3 Links to this page CS247 - Software Engineering Principles