0% found this document useful (0 votes)
4 views51 pages

4b - Operator Overloading

Uploaded by

i.a.m
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views51 pages

4b - Operator Overloading

Uploaded by

i.a.m
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PPTX, PDF, TXT or read online on Scribd
You are on page 1/ 51

Object-Oriented

Programming
Mohammad Salman
Dean’s Fellow
Operator Overloading
An example before we continue on with this topic.

https://www.learncpp.com/cpp-tutorial/introduction-to-operator-overloading/
(SS) What will be the output of this program?

The function is collapsed; the implementation is there.


The implementation of your operator overload should
be intuitive and make sense.

Irresponsible implementation of operator overloads


can serve to confuse your future self and other
programmers working with your code base.

e.g. if you have two string objects, what will their


subtraction indicate?
MyString s1; MyString s2;
std::cout << s1 – s2;
FYI
• Note that doing
fraction1 + fraction2;
triggers a call to operator+().

• You could explicitly call this operator overload as well.


operator+(fraction1, fraction2);

• This will work as well.


Friend Functions
• The class perceives the function marked as friend as its
friend.

• Therefore, the class allows this function to access the private


and protected members (attributes and methods) of its objects.

• This technique can save you from unnecessarily adding many


getters and setters.
Writing Operator Overloads
Using Class Member
Functions
These functions will be part of your class.

https://www.learncpp.com/cpp-tutorial/overloading-operators-using-member-functions/
class Fraction int main()
{ {
private: Fraction fraction1{1, 2}; // 1/2
int m_num;
int m_den;
Fraction fraction2{fraction1}; // 1/2

public: Fraction f3{ fraction1 * fraction2 };


Fraction(int num, int den)
: m_num{num}, m_den{den} f3.show();
{}
return 0;
int getNum() const { return m_num; }
int getDen() const { return m_den; }
}

void show() const


{ std::cout << m_num << '/' << m_den << '\ // Compiler's interpretation
n'; }
};
operator*(fraction1,
fraction2);
// Not part of class; it's present in the global scope.
Fraction operator*(const Fraction& fr1, const Fraction&
fr2)
{
int numRes = fr1.getNum() * fr2.getNum();
int denRes = fr1.getDen() * fr2.getDen();

Fraction res{numRes, denRes};

return res;
The reason we need a single explicit parameter is because the overload is a member function.
To call a member function, you need to call it with an object of that class.
• The first operand gets passed in as the implicit object, and
• The second operand gets passed in as the explicit parameter.

class Fraction
// In main()
{ fraction1 * fraction2
private:
int m_num; // Compiler’s interpretation
int m_den; fraction1.operator*(fraction2
)
public:
// Other stuff here omitted due to lack of space
Fraction operator*(const Fraction& fr2) const
{
int numRes = getNum() * fr2.getNum();
int denRes = getDen() * fr2.getDen();

Fraction res{numRes, denRes};

return res;
}
};
Note // In main()
This first const is only fraction1 * fraction2
there because the
member function has // Compiler’s interpretation
been marked const. fraction1.operator*(fraction2
)

// Member of class Fraction


Fraction operator*(const Fraction& fr2)
const
const Fraction* const this {
int numRes = getNum() * fr2.getNum();
&fraction1
int denRes = getDen() * fr2.getDen();

Fraction res{numRes, denRes};

return res;
}

Fraction fraction1 Fraction fraction2


m_num m_den m_num m_den fr2
// Methods // Methods
Is there a need to use getters in the // Member of class Fraction
implementation of this function? Fraction operator*(const Fraction& fr2)
const
No. It is a member of the class. {
As a member of the class, it can access the int numRes = getNum() * fr2.getNum();
private members of the objects of this class. int denRes = getDen() * fr2.getDen();

Fraction res{numRes, denRes};

return res;
}

Fraction operator*(const Fraction& fr2)


const
{
int numRes = m_num * fr2.m_num;
int denRes = m_den * fr2.m_den;

Fraction res{numRes, denRes};

return res;
}
When overloading an operator using a
member function…
• The left operand becomes the implicit (*this) object.

• All other operands become (explicit) function parameters.


This is multiplication of a Fraction with an int.
class Fraction
{ The fraction1 object is pointed by this, and
private:
int m_num; the integer x gets passed by value into num.
int m_den;

public:
Fraction(int num, int den)
: m_num{num}, m_den{den}
{}

int getNum() const { return m_num; } int main()


int getDen() const { return m_den; } {
Fraction fraction1{1, 2}; //
Fraction operator*(int num) const
{ 1/2
int numRes = m_num * num;
int denRes = m_den; int x{5};
Fraction f3{ fraction1 * x };
Fraction res{numRes, denRes};
// Compiler's interpretation
return res;
// fraction1.operator*(x)
}

void show() const f3.show();


{ std::cout << m_num << '/' << m_den << '\n'; }
}; return 0;
}
How will you write an overload for operator*
class Fraction so that int is the first parameter and Fraction
{ is the second parameter?
private:
int m_num;
int m_den; You cannot.
Need a non-member function.
public:
Fraction(int num, int den)
: m_num{num}, m_den{den}
{}

int getNum() const { return m_num; } int main()


int getDen() const { return m_den; } {
Fraction fraction1{1, 2}; //
Fraction operator*(int num) const
{ 1/2
int numRes = m_num * num;
int denRes = m_den; int x{5};
Fraction f3{ x * fraction1 };
Fraction res{numRes, denRes};
// Compiler's interpretation
return res;
// fraction1.operator*(x)
}

void show() const f3.show();


{ std::cout << m_num << '/' << m_den << '\n'; }
}; return 0;
}
This is what you have.

As a member function overload, this is what the x * fraction1


interpretation should be.

But this doesn’t make sense because an int is


x.operator*(fraction1
not a class in C++. )

As such you need to overload operator*() as x * fraction1


a global (or a non-member) function.

So that we have this interpretation. operator*(x, fraction1)


Can you define the overload for operator<<() as a member function of class Point (or any user-defined
class for that matter)?
std::ostream& operator<< (std::ostream& out, const Point& point)
{
out << "Point(" << point.getX() << ", " << point.getY() << ", " << point.getZ() <<
')';

return out;
}
If you wanted this to be defined as a member function, the interpretation would be as follows.
std::cout <<
point1;
(std::cout).operator<<(point1)
;
So operator<<() shouldn’t be a member of class Point.
Now, we would need to somehow modify the class ostream (to which cout belongs to).
Since ostream is built into the language, we cannot modify it.

Therefore, overloads for operator<<() and for operator>>() (not an exhaustive list) have to be
defined as normal (or non-member) functions.
When overloading an operator using a
member function…
• The left operand becomes the implicit *this object.

• All other operands become (explicit) function parameters.

• The overloaded operator must be added as a member function


of the left operand.
• Use the following examples to understand why this point is important.

x * fraction1 std::cout << point1;


x.operator*(fraction1) (std::cout).operator<<(point1)
;
Operator Overloading
• There are some operators which must be overloaded as
normal (non-member) functions.

• There are some operators which can be overloaded in both


ways.

• There are some operators which must be overloaded as


member functions (they must be part of the class).
Operator Overloading
• There are some operators which must be overloaded as member
functions (they must be part of the class).

• Assignment ( = )
• Subscript ( [] )
• Function call ( () )
• Member selection ( -> )
• And some others.

• This is the requirement of C++.


Overloading operator=

https://www.learncpp.com/cpp-tutorial/overloading-the-assignment-operator/
The Copy Assignment Operator
operator=
• It is used to copy values from one object to another already
existing object.

• It must be overloaded as a member function.


Assignment is done from right to left.

Keep this in mind when chaining the assignment operator.

int x = 5;
int y = 6;
int z = 7;

x = y = z;

std::cout << x << ' ' << y << ' ' <<
z;
Copy Constructor vs. operator=
• The purpose of the copy constructor and the copy assignment
operator are almost equivalent -- both copy one object to
another.

• However, the copy constructor initializes new objects,


whereas the assignment operator replaces the contents of
existing objects.
• Note that the copy ctor will be called when you pass or return objects
by value because new objects are being created.
This, however, works just fine.
The compiler provides you with an int main()
overload for operator= that works with {
objects of the same class. Fraction fiveThirds{ 5,
3 };
class Fraction f.operator=(fiveThirds) Fraction f{ 1, 2 };
{
private: std::cout << f << '\n';
int m_num;
int m_den; f = fiveThirds;
std::cout << f;
public:
Fraction(int num, int den) return 0;
: m_num{ num }, m_den{ den } }
{}

Fraction(const Fraction& fr)


: m_num{ fr.m_num }, m_den{ fr.m_den }
{}

friend std::ostream& operator<<(std::ostream& out, const Fraction&


f1);
};
Fraction& Fraction::operator=(const Fraction& fr)
{
// do the copy
m_num = fr.m_num;
m_den = fr.m_den;

class Fraction
// return the implicit object so we can chain this
{ operator
private: return *this;
int m_num; }
int m_den;

public: Returning by reference in


Fraction(int num, int den) operator= allows the
: m_num{ num }, m_den{ den } assignment operator to be
{}
chained – similar discussion
Fraction(const Fraction& fr) we had for method chaining
: m_num{ fr.m_num }, m_den{ fr.m_den } (when discussing *this) and
{}
cascading << and >>
// Overloaded assignment operators.
Fraction& operator=(const Fraction& fr);

friend std::ostream& operator<<(std::ostream& out, const Fraction&


f1);

};
Fraction& operator= (const Fraction& fraction)
{
// in main() // self-assignment guard
f1 = f1; if (this == &fraction)
// OR return *this;
f1.operator=(f1)
; m_num = fraction.m_num; // can handle self-assignment
class Fraction properly
{ m_den = fraction.m_den; // can handle self-assignment
private: properly
int m_num;
int m_den; return *this;
} Note
public:
Fraction(int num, int den) In fact this is an even better
: m_num{ num }, m_den{ den } implementation.
{} In this example, the self-
assignment guard isn’t doing
Fraction(const Fraction& fr) much. However, there are cases
: m_num{ fr.m_num }, m_den{ fr.m_den }
{}
where it becomes crucial.

// Overloaded assignment Check the reference link


Fraction& operator=(const Fraction& fr); provided and a SS discussion at
the end to figure out.
friend std::ostream& operator<<(std::ostream& out, const Fraction&
f1);

};
We would need to provide the overload
class Fraction for operator= (as a class member) for int main()
{ this to work. {
private: Fraction fiveThirds{ 5,
int m_num; fiveThirds.operator=(4) 3 };
int m_den;

public: fiveThirds = 4;
Fraction(int num, int den)
: m_num{ num }, m_den{ den } return 0;
{} }
Fraction(const Fraction& fr)
: m_num{ fr.m_num }, m_den{ fr.m_den }
{}

friend std::ostream& operator<<(std::ostream& out, const Fraction&


f1);
};

std::ostream& operator<<(std::ostream& out, const Fraction& f1)


{
out << f1.m_num << '/' << f1.m_den;
return out;
}
Currently, you will run into a compile-time error.

There is a nuance here that could make this work without providing the overload for operator=,
but we will have to digress from the main topic. For the interested folks, the nuance relates to
implicit type conversion of the integer into the object itself.
Self Study
What will be the output here?

int main()
class Fraction {
{ Fraction f1 { 5, 3 };
private: Fraction f2 { 7, 2 };
int m_num; Fraction f3 { 9, 5 };
int m_den;
f1 = f2 = f3; // chained
public: assignment
Fraction(int num, int den)
: m_num{ num }, m_den{ den }
{} return 0;
}
Fraction(const Fraction& fr)
: m_num{ fr.m_num }, m_den{ fr.m_den }
{}

Fraction& operator= (const Fraction& fr) = default;

friend std::ostream& operator<<(std::ostream& out, const Fraction&


f1);
};
Finishing the Rule of
Three
Keep in mind that the compiler provides an overload for
operator= if I don’t provide one.

This overload is only for the objects of that same class.

https://www.learncpp.com/cpp-tutorial/shallow-vs-deep-copying/
class MyList The Rule of Three guideline: If you find yourself defining
{ either the copy ctor, or the dtor, or that third thing, it’s
private: recommended to define the other two as well.
double* m_array;
int m_length;
But what is that third thing that we need to be mindful about?
public:
MyList(int length) int main()
: m_array{ new double[length] }, {
m_length{ length } MyList list1{ 5 };
{}
{
MyList(const MyList& ob)
: m_array{ new
MyList
double[ob.m_length] }, list2{ list1 };
m_length{ ob.m_length } }
{
for(int i{0}; i < ob.m_length; ++i) return 0;
{ }
m_array[i] = ob.m_array[i];
}
}

~MyList()
{
delete[] m_array;
}
class MyList Will the copy ctor be called here?
{ No!
private: The two objects have already been created, I’m just
double* m_array;
copying over the attributes of list2 into list1.
int m_length;

public:
MyList(int length) int main()
: m_array{ new double[length] }, {
m_length{ length } MyList list1{ 5 };
{} MyList list2{ 3 };
MyList(const MyList& ob)
: m_array{ new
list2 = list1;
double[ob.m_length] },
m_length{ ob.m_length } return 0;
{ }
for(int i{0}; i < ob.m_length; ++i)
{
m_array[i] = ob.m_array[i];
The following slide shows how list1 and list2
}
}
will look like before the assignment operation.

~MyList()
{
delete[] m_array;
}
Stack Memory Heap Memory
Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20

double* = 0x130 int = 3 12.0 91.5 -10.4


list2 Methods 0x130
0x100
class MyList The compiler’s provided operator= will be used, which
{ just copies members of list1 into list2.
private:
double* m_array;
int m_length;

public:
MyList(int length) int main()
: m_array{ new double[length] }, {
m_length{ length } MyList list1{ 5 };
{} MyList list2{ 3 };
MyList(const MyList& ob)
: m_array{ new
list2 = list1;
double[ob.m_length] },
m_length{ ob.m_length } return 0;
{ }
for(int i{0}; i < ob.m_length; ++i)
{
m_array[i] = ob.m_array[i];
What this does, is shown on the next slide.
}
}

~MyList()
{
delete[] m_array;
}
Stack Memory Heap Memory
Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20

This memory has been leaked!

double* = 0x40 int = 5 12.0 91.5 -10.4


list2 Methods 0x130
0x100
Additionally, we have coupled list1 and list2, and also
when one object is destroyed, the other’s destruction will
cause problems.
Similar problems that we saw while studying about copy ctors.
class MyList
Therefore, we need to write our own operator= for
{ objects of the same class (MyList in this e.g.).
private:
double* m_array; It will be a member function.
int m_length;

public: Let’s think about its implementation. It can’t be just


MyList(int length) copy-pasting values of list1 into list2.
: m_array{ new double[length] },
m_length{ length }
{}

MyList(const MyList& ob) list2 = list1;


: m_array{ new double[ob.m_length] },
m_length{ ob.m_length }
{ // Compiler's interpretation
for(int i{0}; i < ob.m_length; ++i) list2.operator=(list1);
{
m_array[i] = ob.m_array[i];
}
}

MyList& operator=(const MyList& source);

~MyList()
{
delete[] m_array;
}
};
list2 = list1;
This is our state before assignment. Now, let’s try to figure out the
implementation for operator=. // Compiler's interpretation
list2.operator=(list1);

Stack Memory Heap Memory


Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20

double* = 0x130 int = 3 12.0 91.5 -10.4


list2 Methods 0x130
0x100
list2 = list1;
MyList& MyList::operator=(const MyList& source)
{
// Compiler's interpretation
// first we need to deallocate any value that this array is holding.
list2.operator=(list1);
delete[] m_array;
m_array = nullptr;

// because m_length is not a pointer, we can shallow copy it


m_length = source.m_length;

// m_array is a pointer, so we need to deep copy it if it is non-null


if (source.m_array != nullptr)
{
// allocate memory
m_array = new double[m_length];

// do the copy
for (int i{ 0 }; i < m_length; ++i)
{
m_array[i] = source.m_array[i];
}
}
else
{
// this->m_array is already nullptr
}

// return by reference
return *this;
}
list2 = list1;

// Compiler's interpretation
list2.operator=(list1);

Stack Memory Heap Memory


Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20

double* = nullptr int = 5


list2 Methods
0x100
list2 = list1;
MyList& MyList::operator=(const MyList& source)
{
// Compiler's interpretation
// first we need to deallocate any value that this array is holding.
list2.operator=(list1);
delete[] m_array;
m_array = nullptr;

// because m_length is not a pointer, we can shallow copy it


m_length = source.m_length;

// m_array is a pointer, so we need to deep copy it if it is non-null


if (source.m_array != nullptr)
{
// allocate memory
m_array = new double[m_length];

// do the copy
for (int i{ 0 }; i < m_length; ++i)
{
m_array[i] = source.m_array[i];
}
}
else
{
// this->m_array is already nullptr
}

// return by reference
return *this;
}
list2 = list1;

// Compiler's interpretation
list2.operator=(list1);

Stack Memory Heap Memory


Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20

double* = 0x200 int = 5 5.1 9.0 -1.4 3.1 0.0


list2 Methods 0x200
0x100
list2 = list1;
MyList& MyList::operator=(const MyList& source)
{
// Compiler's interpretation
// first we need to deallocate any value that this array is holding.
list2.operator=(list1);
delete[] m_array;
m_array = nullptr;

// because m_length is not a pointer, we can shallow copy it


m_length = source.m_length;

// m_array is a pointer, so we need to deep copy it if it is non-null


if (source.m_array != nullptr)
{
// allocate memory
m_array = new double[m_length];

// do the copy
for (int i{ 0 }; i < m_length; ++i)
{
m_array[i] = source.m_array[i];
}
}
else
{ • Note that when doing delete[] m_array at
// this->m_array is already nullptr
}
the very start of this function, m_array could
be a nullptr.
// return by reference • Note that it is safe to delete a nullptr;
return *this; nothing will happen.
}
list2 = list1;
MyList& MyList::operator=(const MyList& source)
{
// Compiler's interpretation
if (this == &fraction)
list2.operator=(list1);
return *this;

// rest of the deep copy logic we just saw will follow.


}

• Make sure you guard against self-assignment.


Self Study
• Think about the implementation of a copy ctor vs. the implementation
of operator=.

• What are the differences?

• What are the similarities?

• Since the implementations are similar, you can actually create a


separate private member function (called deepCopy() let’s say) and
call that within your copy ctor and operator= to prevent rewriting the
same code.
Rule of Three (finally!)
• If a class requires a user-defined
• destructor,
• copy constructor, or
• copy assignment operator,
• then it probably requires all three.

• If we’re user-defining any of these functions, it’s probably because we’re


dealing with dynamic memory allocation.

• We need the copy constructor and copy assignment to handle deep


copies, and the destructor to deallocate memory.
Rule of Three (finally!)
• Generally, it would be better to rely on the built-in classes that
you have available in C++.

• std::vector
• std::string

• They take care of memory management for you.


• You should use these in your projects as well.
Rule of Three (finally!)
• In modern C++, there is actually a Rule of Five, which is
related to the idea of move semantics.

• Move semantics and Rule of Five are not part of this course.
(SS) In our own implementation of operator=, assume the check for self-guard didn’t exist, and you did this:
list1 = list1;

Stack Memory Heap Memory


Automatic Memory Allocation Dynamic Memory Allocation

double* = 0x40 int = 5 5.1 9.0 -1.4 3.1 0.0


list1
Methods 0x40
0x20
(SS) In our own implementation of operator=, assume the check for self-guard didn’t exist, and you did this:
list1 = list1;

Based on this implementation, you first deallocate the array associated


with list1 (delete[] m_array).

Then, within the conditional, you will not reallocate the array since
source.m_array (which is also m_array in the case of self-
assignment), is nullptr. So a self-assignment causes the array
associated with list1 to be freed (which doesn’t make sense).

If we had not set m_array to nullptr after deallocating, you could


argue that the conditional would be satisfied and the new array
allocated, but then what is happening within the loop?

The array you just allocated is not initialized (or could be initialized to
zero). In either case, you have lost your original information, which
again, doesn’t make sense.

Having a guard against self-assignment, preserves our object in its


original state.

You might also like