Docsity
Docsity

Prepare for your exams
Prepare for your exams

Study with the several resources on Docsity


Earn points to download
Earn points to download

Earn points by helping other students or get them with a premium plan


Guidelines and tips
Guidelines and tips

Operator Overloading in C++: A Detailed Guide to Non-Member and Member Functions, Study Guides, Projects, Research of Object Oriented Programming

Learn how to overload operators in C++ for both non-member and member functions. Understand the syntax, examples, and benefits of operator overloading for classes like CoinMoney. Discover how to add, multiply, and define unary operators.

What you will learn

  • What are the benefits of operator overloading in C++?
  • How do you define a member operator function in C++?
  • What is the difference between a non-member and member operator function in C++?
  • How do you define a non-member operator function in C++?
  • What is operator overloading in C++?

Typology: Study Guides, Projects, Research

2021/2022

Uploaded on 09/27/2022

aeinstein
aeinstein 🇺🇸

4.6

(22)

259 documents

1 / 14

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
A Summary of Operator Overloading
David Kieras, EECS Dept., Univ. of Michigan
Prepared for EECS 381
8/27/2013
Basic Idea
You overload an operator in C++ by defining a function for the operator. Every operator in the
language has a corresponding function with a name that is based on the operator. You define a
function of this name that has at least one parameter of the class type, and returns a value of
whatever type that you want. Because functions can have the same name if they have different
signatures, the compiler will apply the correct operator function depending on the types in the
call of it.
Rule #1. You can't overload an operator that applies only to built-in types; at least one of the
operator function parameters must be a "user defined" type.
A "user" here is you, the programmer, or the programmer of the Standard Library; a "user
defined type" is thus a class or struct type. This means you can't overload operator+ to redefine
what it means to add two integers. Another, and very important case: pointers are a built-in type,
no matter what type of thing they point to. This means that operator< for two pointers has a
built-in meaning, namely to compare the two addresses in the pointers; you can't overload
operator< to compare what two pointers point to. The Standard Library helps you work around
this limitation.
You define operator functions differently depending on whether the operator function is a
member of your own type's class, or is a non-member function. A further variation is whether
the arguments are of the same or different types. We'll do this first for non-member functions,
then for member functions.
In schematic form, when you use a binary operator (op), there is a left-hand-side operand (lhs)
and a right-hand-side operand (rhs), and the whole expression has a value.
lhs op rhs --- has a value resulting from applying op to lhs and rhs
The operator function's first argument is the lhs operand, and the second is the rhs operand.
The name of the operator function is operator op, where op is the operator, such as '+', '*', etc.
The unary operators are almost all special cases, described later in this handout.
1
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe

Partial preview of the text

Download Operator Overloading in C++: A Detailed Guide to Non-Member and Member Functions and more Study Guides, Projects, Research Object Oriented Programming in PDF only on Docsity!

A Summary of Operator Overloading

David Kieras, EECS Dept., Univ. of Michigan

Prepared for EECS 381

Basic Idea

You overload an operator in C++ by defining a function for the operator. Every operator in the

language has a corresponding function with a name that is based on the operator. You define a

function of this name that has at least one parameter of the class type, and returns a value of

whatever type that you want. Because functions can have the same name if they have different

signatures, the compiler will apply the correct operator function depending on the types in the

call of it.

Rule #1. You can't overload an operator that applies only to built-in types; at least one of the

operator function parameters must be a "user defined" type.

A "user" here is you, the programmer, or the programmer of the Standard Library; a "user

defined type" is thus a class or struct type. This means you can't overload operator+ to redefine

what it means to add two integers. Another, and very important case: pointers are a built-in type,

no matter what type of thing they point to. This means that operator< for two pointers has a

built-in meaning, namely to compare the two addresses in the pointers; you can't overload

operator< to compare what two pointers point to. The Standard Library helps you work around

this limitation.

You define operator functions differently depending on whether the operator function is a

member of your own type's class, or is a non-member function. A further variation is whether

the arguments are of the same or different types. We'll do this first for non-member functions,

then for member functions.

In schematic form, when you use a binary operator (op), there is a left-hand-side operand (lhs)

and a right-hand-side operand (rhs), and the whole expression has a value.

lhs op rhs --- has a value resulting from applying op to lhs and rhs

The operator function's first argument is the lhs operand, and the second is the rhs operand.

The name of the operator function is operator op, where op is the operator, such as '+', '*', etc.

The unary operators are almost all special cases, described later in this handout.

Non-member operator functions

Rule #2. A non-member operator overload function is just an ordinary function that takes at

least one argument of class type and whose name is of the form operator op.

A non-member operator overloading function simply has the operator function name and does

whatever you want with the lhs and rhs parameters. For example, suppose we have a simple

CoinMoney class that represents money that consists of collections of nickels, dimes, and

quarters. A CoinMoney object stores the number of coins of each type, and computes the

monetary value on request: A sketch of this class, leaving out members not immediately relevant,

is as follows:

class CoinMoney { public: ! CoinMoney(int n = 0, int d=0, int q= 0) : !! nickels(n), dimes(d), quarters(q) {} ! int get_value() const !! {return nickels * 5 + dimes * 10 + quarters * 25;} private: ! int nickels; ! int dimes; ! int quarters; };

Suppose we want to be able to add two CoinMoney objects and get a third CoinMoney object

that has the sum of the number of nickles, dimes, and quarters from the two objects. We define

the function named operator+ that takes two arguments of CoinMoney type and returns a

CoinMoney object with the correct values. Because the CoinMoney members are private, they

will not be available to a non-member version of the function. We need to either add a friend

declaration or a bunch of readers functions to the class. Let's assume we have the readers. The

operator+ definition would then be:

CoinMoney operator+ (const CoinMoney& lhs, const CoinMoney& rhs) { ! CoinMoney sum ( !! (lhs.get_nickels() + rhs.get_nickels()), !! (lhs.get_dimes() + rhs.get_dimes()), !! (lhs.get_quarters() + rhs.get_quarters()) !! ) ! return sum; }

This function creates a new object initialized with the nickels value from the lhs and rhs objects,

and likewise for dimes and quarters, and returns the new object by value.

So now we can write sums of CoinMoney objects:

! m3 = m1 + m2;

class CoinMoney { ... ! // declare the operator overload as a const member function ! CoinMoney operator+ (const CoinMoney& rhs) const; ... }; CoinMoney CoinMoney::operator+ (const CoinMoney& rhs) const { ! CoinMoney sum( !! (nickels + rhs.nickels), !! (dimes + rhs.dimes), !! (quarters + rhs.quarters) !! ); ! return sum; }

The naked "nickels" is the member variable in the left-hand-side operand, "this" object. The

function has only one parameter, the right-hand-side operand. Because this function is a member

of the class, it has direct access to the member variables in "this" current object, and dot access to

the member variables in the other objects in the same class.

Since a member operator function is just an ordinary member function, the following statements

do the same thing:

! m3 = m1 + m2; ! m3 = m1.operator+ (m2);

The member version of an operator function is called to work on the left-hand operand, with the

right-hand operand being the function argument. Again, calling operator functions explicitly is

legal, but rare, and usually pointless.

Left-hand operand for member operator functions must be the class type.

Rule #3. An operator overload function written as a member function can only be applied if the

lhs is of the class type.

As with non-member operator overload functions, you don't have to have both arguments be the

same type. However, by definition, the left-hand operand for a member operator function must

be an object of the class that the function is a member of.

For example, suppose we wanted to be able to multiply CoinMoney objects by doubles as in the

above example. We can define operator* as a member function only if a CoinMoney object is

the left-hand operand, but not if a double is the left-hand operand.

That is, CoinMoney operator* (double x) can be defined as a member function of

CoinMoney, and so

m1.operator* (2.5);

is a legal call. But there is no way to write operator* as a member function of CoinMoney so that

you could write either one of

x = my_double_variable * m1;

x = my_double_variable.operator* (m1);

or if Thing is some other class type, the same applies:

x = my_thing * m1;

You can see why - if operator* is a member of CoinMoney, the lhs has to be a CoinMoney, not

a double or a Thing. What do you do in such cases? Simple: define this version of the operator

overload using a non-member function.

Then the member function handles

! m2 = m1 * my_double_variable;

and the non-member function handles

! m2 = my_double_variable * m1;

Which operators can and should be overloaded?

Almost all of the operators can be overloaded. But that doesn't mean you should overload them!

Good OOP practice is to overload operators for a class only when they make obvious sense. For

example, what would less-than mean for CoinMoney?

! m1 < m

There are several reasonable ways that one collection of coins could be considered to be less than

another - total number, total value, number of highest value coin, even total weight! You would

define this operator only if there was only one reasonable interpretation in the problem domain

you are working in. Finally, there is no clue what some operators might mean, such as:

! (m1 % m2++) | m

Note that you don't have to define both "ways" for an overloaded operator. For example, maybe

you want halve the value of the coins in a CoinMoney object:

! m1 / 2.

6. In the body of this function, apply the ordinary output operator to the supplied stream

parameter instead of cout. This allows the the operator to work with other streams. For example,

os << x.nickels;

What if I have pointers instead of objects?

Often we have containers of pointers to objects in addition to objects; in such cases it can be very

convenient to define an additional overload of the output operator to take a pointer to an object.

This can be trivially implemented in terms of the usual overload of the object type. For example,

if we have pointers to CoinMoney objects, we can define:

ostream& operator<< (ostream& os, const CoinMoney* p) { ! os << *p; ! return os; }

and then if we have CoinMoney* m1_ptr; we can write:

cout << m1_ptr << endl;

Type safety from overloaded operators

The ostream class in the Standard Library includes an overload of operator<< for every built-in

type, as in:

ostream& operator<< (ostream& os, int x) {/* output an integer /} ostream& operator<< (ostream& os, double x) {/ output a double /} ostream& operator<< (ostream& os, char * x) {/ output a C string */}

This is why output using the iostream library is type-safe - the compiler will make sure the right

output function is called for the type of object you are outputting.

How do I overload the input operator to input objects of my own type?

Overloading the input operator is very similar to overloading the output operator, but it is less

often done – usually, objects are created and have their values set using a constructor, rather than

being first created and then having their member variables set from file or keyboard input.

The key issues in writing an overloaded input operator are deciding whether you are going to

insist on a special format for the input, and how you are going to deal with erroneous input. For

present purposes, we will ignore these problems and assume for the sake of example that we will

read a CoinMoney object as three integers separated by whitespace. The basic pattern for

overloading the input operator is illustrated with this example:

istream& operator>> (istream& is, CoinMoney& m) { ! /* whatever you want to input and store in m */ ! is >> m.nickels >> m.dimes >> m.quarters; ! return is; }

Such an operator definition would allow you to write code like:

CoinMoney m1; cin >> m1;! // read member variable values ...

Here are the things you have to get right:

1. The second parameter, whose type is your own class, also has to be a reference-type

parameter, because you will be storing values in the caller's object. A call-by-value parameter

would just be a copy of the caller's object; you would store values in the copy, and then it would

get thrown away when the function returned. Oops! So this function has to modify the caller's

object.

2. The operator>> function can't be a member of your own class, because the left-hand operand

is an istream object.

3. If the operator>> function is going to set private member variables of your own class, it will

need to either have friend status (assumed here), or use public writer functions.

4. The first parameter, is, has to be a reference-type parameter because you want is to be the

very same stream object that the operator is applying to, and not a copy of it. Inside the function,

is is an alias, another name, for the original cin object.

5. The return type has to be a reference to an istream object, so that each application of >> will

produce the same istream object that was originally on the left-hand side, so the next >> will

take it as its left-hand operand. This is why you can cascade the input operator.

6. You have to be sure to return the istream parameter object so that the cascading in will work.

The Standard Library also includes an overloaded input operator for every built-in type, again

resulting in type-safety. The use of the reference parameter for the destination argument makes it

unnecessary to supply an address, as in C's scanf. In fact, getting neat and clean operator

overloading is a major reason why reference parameters were included in C++.

For the prefix version, the meaning is: increment the value before using it. Implementing this

operator is simple: just increment the relevant member variable(s) then return this object by

reference by simply returning *this.

  • The postfix operator++ is the "odd" one. It has a dummy integer parameter which is never used,

but the different signature serves to distinguish its operator function from the prefix operator

function. Note that an unused parameter does not need a variable name - in fact this is the idiom

for saying that you have an unused parameter in a function, and the compiler will not complain

about an unused parameter designated this way. So the postfix increment operator's signature

would be

CoinMoney operator++ (CoinMoney&, int) if a non-member,

CoinMoney operator++ (int) if a member.

For the postfix version, the meaning is: increment the value after using it, where by "after using

it" we mean: make arrangements to set aside the previous value, then increment the member

variable(s), then return the previous value for the calling code to use. This means you have to

declare a local variable to hold the previous value so that you can return it after doing the

increment. You return this local variable by value (not by reference). You can see how postfix++

can be slower to execute than prefix++.

The same rules apply to the decrement operator, "--". Notice that these operator overloads

modify their objects, so they can't be const member functions or have const parameters.

Conversion operators - letting the compiler convert types for you

In the CoinMoney example, we might want to treat a CoinMoney object as a single numeric

value, say as a double, so we could compute the sales tax for a CoinMoney amount by:

tax = .06 * m1;

where we want to multiply .06 times the value of m1. If we tell the compiler how to turn a

CoinMoney object into a double, then the compiler can just generate code for the multiply easily.

We can do this by defining a conversion operator, which is a function whose name is operator

and whose return type is always the and so is not stated. We would write (say as

a member function):

operator double () const { ! return get_value(); }

Conversion operators are notorious for surprising the programmer. For example, if we had both

this conversion operator and the operator* (double, CoinMoney) we now have an ambiguity

in how the compiler should analyze the above tax statement, producing an error message. So use

these very sparingly. Usually a specialized get_ function is a better choice because it makes the

intent more clear.

The istream and ostream classes typically contain a conversion operator to convert a istream

or ostream object into a bool, so that you can write:

if(my_input_file) {/* everything is good! */}

You aren't testing for whether the humongous istream object is true or nonzero - it's a big object

jammed full of data! Rather the function

operator bool (const istream& is) {whatever}

computes a true/false value based on the stream state bits, thereby allowing you to treat the

stream object as a single true/false value.

The compiler can use a constructor to convert types - the explicit keyword

Sometimes a constructor function plays the role of a conversion function. For example, consider

the following code sketch:

class Thing { blah blah }; class Glob { ! Glob(Thing t); // construct a Glob from a Thing blah blah }; void foo(Glob g); // function foo takes a Glob parameter .... Thing my_thing; ... foo(my_thing); // call foo with a Thing? ...

You can't call foo with a Thing as an argument; foo requires a Glob. But the compiler cleverly

notices that it can construct a Glob from a Thing, so it compiles the call as if the programmer had

written:

foo (Glob(my_thing));

This is called an implicit conversion. Often this is exactly what you want. Sometimes it is a

source of mysterious errors. The keyword explicit is used to prevent this use of a constructor,

meaning that you want the constructor used only for the purpose of explicitly constructing one

type from another. So if the Glob(Thing) constructor was defined as follows:

The syntax for the function call operator is based on function declaration and function call

syntax. You declare a function by naming a return type, a function name, and then a list of

parameter type-name pairs enclosed in parentheses. You call a function by providing the name

followed by a list of arguments enclosed in parentheses. What bit of syntax stands out here? The

parenthesized list of parameters or arguments! So the name of the function call operator is

simply: operator()

The function call operator must always be a member function of the class, meaning that the

hidden "this" parameter is always the first parameter. Define a function call operator as a class

member by providing a return type, the operator name, then a parenthesized list of parameters

followed by the function definition:

return-type operator() (parameters) {function body}

Here is an example in which the function call operator takes two ints and returns a bool that is

true if the sum of the two ints is odd, false if it is even. The code using it first declares an object

of that type, and then applies it to input from the user.

// declare a function object class with function call operator class My_FOC { public: ! bool operator() (int i1, int i2) const ! { !! int sum = i1 + i2; !! return sum % 2; ! } }; int main() { ! My_FOC odd_tester;// create the function object ! int i1, i2; ! cin >> i1 >> i2;! // no error check - for brevity here ! // apply the function object just like a function ! bool result = odd_tester(i1, i2); ! if(result) !! cout << "sum is odd" << endl; ! else !! cout << "sum is even" << endl; ! return 0; }

It is customary to declare the function call operator as a const member function if it does not

modify the state of its object - which is the case in this example.

In this simple example, the function object doesn't do any more than you can do with simple

function, or if you wanted to be more complicated, a function and a function pointer. However,

notice that the creation of the function object is syntactically much simpler than declaring and

initializing a function pointer - the class declaration and the function object declaration have all

the information that the compiler needs to set up the call to the function code. So even if you

could do it with a function pointer, the function object approach is generally easier to get right.

But because function objects can have other member variables and functions, they are potentially

much more powerful and useful than simple functions and function pointers.