CIS 330 C++ and UnixLecture 15
Operator Overloading
Operator Overloading
Syntactic sugar - simply another way of calling a function
Instead of arguments appearing inside ( … ), they surround the operator
You can define new operators that work with specific classes
● For example, “adding” two classes may have some semantic meaning
● Define the “+” operator to do this (which will then call a function)
WarningDo not overuse operator overloading
Only use it if it makes sense, AND makes it easier to read your code
You cannot overload operators that are used with built-in types
● For example, you cannot change the meaning of + in 5 + 10
DefinitionDefine it like a regular function, but with
operator@
Where @ is the operator you want to overload
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer operator+(const Integer& rv) const {
Integer(i + rv.i);
}
Integer& operator+=(const Integer& rv) {
i += rv.i;
return *this; // l-value
}
};
int main() {
cout << "built-in types:" << endl;
int i = 1, j = 2, k = 3;
k += i + j;
cout << "user-defined types:" << endl;
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
}
Example
class Integer {
int i;
public:
Integer(int ii) : i(ii) {}
const Integer operator+(const Integer& rv) const {
Integer(i + rv.i);
}
Integer& operator+=(const Integer& rv) {
i += rv.i;
return *this; // l-value
}
};
int main() {
cout << "built-in types:" << endl;
int i = 1, j = 2, k = 3;
k += i + j;
cout << "user-defined types:" << endl;
Integer ii(1), jj(2), kk(3);
kk += ii + jj;
}
Example The operator+ produces a new Integer (a temporary) that is used as the rv argument for the operator+=.
The temporary is destroyed when it is no longer needed
Return Value
Member function operator is called for the object on the left-hand side (LHS) of the operator
The argument will be the right-hand side (RHS) of the operator
For non-conditional operators (conditionals usually return a boolean), you will almost always want to return an object, or a reference to an object of the same class/type
● If they are NOT the same type, the interpretation of what is should produce is up to you (e.g., for classes that store charand int, what should char + int produce?)
Overloadable Operators
You can overload almost all the operators in C
● But their use is fairly restrictive. For example, ● you cannot combine operators that have no meaning in C
(e.g., ** to represent exponentiation),● you cannot change the order of evaluation precedence,● you cannot change the number of arguments required
Two methods
● Define it as a global functions (and use friend to allow access)
● Define it as a member function
class Integer {
long i;
Integer* This() { return this; }
public:
Integer(long ll = 0) : i(ll) {}
// No side effects takes
// const& argument:
friend const Integer&
operator+(const Integer& a);
friend const Integer
operator-(const Integer& a);
friend const Integer
operator~(const Integer& a);
friend Integer*
operator&(Integer& a);
friend int
operator!(const Integer& a);
// Side effects have non-const&
// argument:
// Prefix:
friend const Integer&
operator++(Integer& a);
// Postfix:
friend const Integer
operator++(Integer& a, int);
// Prefix:
friend const Integer&
operator--(Integer& a);
// Postfix:
friend const Integer
operator--(Integer& a, int);
};
Unary Operators(Global)
const Integer& operator++(Integer& a) {
cout << "++Integer\n";
a.i++; // a is changed
return a; // a is returned
}
const Integer operator++(Integer& a, int) {
cout << "Integer++\n";
Integer before(a.i);
a.i++; // a is changed
return before; // copy of a before change is returned
}
const Integer& operator--(Integer& a) {
cout << "--Integer\n";
a.i--;
return a;
}
const Integer operator--(Integer& a, int) {
cout << "Integer--\n";
Integer before(a.i);
a.i--;
return before;
}
const Integer& operator+(const Integer& a) {
cout << "+Integer\n";
return a; // Unary + has no effect
}
const Integer operator-(const Integer& a) {
cout << "-Integer\n";
return Integer(-a.i); // Create a new Integer object
}
const Integer operator~(const Integer& a) {
cout << "~Integer\n";
return Integer(~a.i);
}
Integer* operator&(Integer& a) {
cout << "&Integer\n";
return a.This(); // what happens if we make this const?
}
int operator!(const Integer& a) {
cout << "!Integer\n";
return !a.i;
}
Unary Operators(Global)
class Byte {
unsigned char b;
public:
Byte(unsigned char bb = 0) : b(bb) {}
const Byte& operator+() const {
cout << "+Byte\n"; return *this;
}
const Byte operator-() const {
cout << "-Byte\n"; return Byte(-b);
}
const Byte operator~() const {
cout << "~Byte\n"; return Byte(~b);
}
Byte operator!() const {
cout << "!Byte\n"; return Byte(!b);
}
Byte* operator&() {
cout << "&Byte\n"; return this;
}
const Byte& operator++() { //pre
cout << "++Byte\n";
b++; return *this;
}
const Byte operator++(int) { //post
cout << "Byte++\n";
Byte before(b);
b++; return before;
}
const Byte& operator--() { //pre
cout << "--Byte\n";
--b; return *this;
}
const Byte operator--(int) { //post
cout << "Byte--\n";
Byte before(b);
--b; return before;
}
};
Unary Operators(Member)
No argument
Why are these different?
++ and --
You want to be able to call different functions, depending on whether it’s ++a (pre) or a++ (post)
● ++a generate a call to operator++(a)● a++ generate a call to operator++(a, int)● This is done simply to differentiate the functions - the second
int for a++ does not get used
Binary Operators(Global)
class Integer {
long i;
public:
Integer(long ll = 0) : i(ll) {}
// Operators that create new,
// modified value:
friend const Integer
operator+(const Integer& left,
const Integer& right);
friend const Integer
operator<<(const Integer& left,
const Integer& right);
…
// Assignments modify & return lvalue:
friend Integer&
operator+=(Integer& left,
const Integer& right);
…
// Conditional operators return true/false:
friend int
operator==(const Integer& left,
const Integer& right);
…
void print(std::ostream& os) const { os << i; }
};
// Operators that create new,
// modified value:
const Integer
operator+(const Integer& left,
const Integer& right) {
return Integer(left.i + right.i);
}
const Integer
operator<<(const Integer& left,
const Integer& right) {
return Integer(left.i << right.i);
}
Binary Operators(Global)
// Assignments modify & return lvalue:
Integer& operator+=(Integer& left,
const Integer& right) {
if(&left == &right) {
/* self-assignment */}
left.i += right.i;
return left;
}
// Conditional operators return true/false:
int operator==(const Integer& left,
const Integer& right) {
return left.i == right.i;
}
For example, (a+=1)++; is legal, but (++a)++; is NOT legal in C++
class Byte {
unsigned char b;
public:
Byte(unsigned char bb = 0) : b(bb) {}
// No side effects: const member function:
const Byte
operator+(const Byte& right) const {
return Byte(b + right.b);
}
…
const Byte
operator<<(const Byte& right) const {
return Byte(b << right.b);
}
…
Binary Operator(Member)
Byte& operator=(const Byte& right) {
// Handle self-assignment:
if(this == &right) return *this;
b = right.b;
return *this;
}
…
Byte& operator+=(const Byte& right) {
if(this == &right) {/* self-assignment */}
b += right.b;
return *this;
}
…
int operator==(const Byte& right) const{
return b == right.b;
}
…
};
operator= is ONLY allowed to be a member function
Assignments operators (e.g., operator+=) have code to check for self-assignment (although it does not do anything)
● This is a general guideline - there are cases where self-assignment is required (e.g., A+=A to add to itself)
● However, for operator= (depending on what the “=” means) you may have to handle self-assignment as a separate case
Binary Operator
Summary
Unary
● Global vs. Member● friend const Integer operator-(const Integer& a); vs.● const Byte operator-() const
● Differentiate pre- and post- operator using different function definition● friend const Integer& operator++(Integer& a);● friend const Integer operator++(Integer& a, int);
Binary
● Global vs. Member● friend Integer& operator+=(Integer& left, const Integer& right); vs.● Byte& operator+=(const Byte& right);
● operator= is only allowed as a member overloaded function
Arguments and Return Values
You can pass it in any way you want
● You just deal with bugs later
However, the better practice is to restrict what you can do with them depending on what the operator requires
● For example, if you only need to read from the arguments, default to passing it as const reference● Ordinary operators like +, -, conditionals, typically do not
change their arguments, so need to be passed in as const reference
● If they are member functions make it a constmember function
For assignment operators (e.g., +=, =) that change the left-hand argument, the left-hand side is NOT a const
Arguments and Return Values
Type of return value depends on the expected “meaning” of the operation
All assignment operators modify the left-hand value (l-value)
● To allow this to be used in chained expression (e.g., a = b= c;), it is expected that reference to the l-value that was modified is returned
● Since a = b = c; is read from left to right by the compiler, you CAN have it return const, but if you want to perform an operation on it (e.g., (a = b).func(); to call func() on a after assigning b to it), the return value should be non-const reference (remember that you can’t call non-constmember functions on a const object).
For logical operators, everyone expects int at worst, and bool at best
More on Return Value as const
Consider func(a + b)
● a + bwill be automatically stored as a const because it is a temporary - so making the return value constmay seem redundant
● Also, you may want to do (a + b).func2()● Now, only a const function would be executed if the return
value is const● This is actually the correct thing to do - why?
● (a + b) isn’t explicitly stored anywhere - so this prevents you from storing potentially valuable information on an object that will likely be lost
● For example
(a + b).func2(); // increment the result by 1
(a + b).func2(); // increment the result by 1
What would be the end result?
Return Value Optimization
return Integer(left.i + right.i);
● This is NOT a function call to a constructor (we have seen this format before in aggregate init)
● This actually means, make a temporary Integer object and return it
● This different fromInteger tmp(left.i + right.i);return tmp;
● tmp object is created using its constructor -> copy-constructor copies its value to where the return value is stored -> destructor is called for tmp
● This is less efficient than the first method● Compiler directly creates the object into the return value
location (i.e., 1 constructor call, no copy-constructor, no destructor)
Unusual Operators
operator[]must be a member function
● Object being called is assumed to act like an array, so you will often return a reference
● Thus, it can be used on the LHS of an equal sign
new and delete can also be overloaded in different ways
Unusual Operators
operator->
Generally used to make the object appear to be a pointer
Overloading typically results in adding more functionality to what it usually means, and is often referred to as a “smart pointer”
Used to implement iterators, that allows you to move through a collection of objects one at a time without providing direct access (i.e., safer)
Also must be a member function
class SmartPointer {
ObjContainer& oc;
int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++(); // Use prefix version
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned bySmartPointer::operator->()");
return oc.a[index];
}
};
class Obj {
static int i, j;
public:
void f() const { cout << i++ << endl; }
void g() const { cout << j++ << endl; }
};
int Obj::i = 47;
int Obj::j = 11;
// Container:
class ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
friend class SmartPointer;
};
Example
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
SmartPointer sp(oc); // Create an iterator
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(sp++);
} ///:~
Example
Class Obj is as beforeclass ObjContainer {
vector<Obj*> a;
public:
void add(Obj* obj) { a.push_back(obj); }
class SmartPointer;
class SmartPointer {
ObjContainer& oc;
unsigned int index;
public:
SmartPointer(ObjContainer& objc) : oc(objc) {
index = 0;
}
// Return value indicates end of list:
bool operator++() { // Prefix
if(index >= oc.a.size()) return false;
if(oc.a[++index] == 0) return false;
return true;
}
bool operator++(int) { // Postfix
return operator++();
}
Obj* operator->() const {
require(oc.a[index] != 0, "Zero value "
"returned by SmartPointer::operator->()");
return oc.a[index];
}
};
// Function to produce a smart pointer that points to the beginning of the ObjContainer:
SmartPointer begin() {
return SmartPointer(*this);
}
};
Nested Iterator
int main() {
const int sz = 10;
Obj o[sz];
ObjContainer oc;
for(int i = 0; i < sz; i++)
oc.add(&o[i]); // Fill it up
ObjContainer::SmartPointer sp = oc.begin();
// SmartPointer sp(oc);
do {
sp->f(); // Pointer dereference operator call
sp->g();
} while(++sp);
} ///:~
Nested Iterator
In C, you can define function pointers
void qsort(void *base, size_t nmemb, size_t size,int(*compar)(const void*, const void*));
int int_sorter(const void *first_arg, const void *second_arg)
{
int first = *(int*)first_arg;
int second = *(int*)second_arg;
if ( first < second )
return -1;
else if ( first == second )
return 0;
else
return 1;
}
qsort(base, n, size, &int_sorter);
typedef and function pointers
typedef and function pointers
Similar in C++, you can make this a bit cleanervoid qsort(void *base, size_t nmemb, size_t size,
int(*compar)(const void*, const void*));int (*comp) (const void*, const void*) = &int_sorter;
Can be written as
typedef int (*compFnc)(const void*, const void*);
void qsort(void *base, size_t nmemb, size_t size, compFnc comp);
compFnc comp = &int_sorter;
qsort(base, n, size, comp);
operator->*class Dog {
public:
int run(int i) const {
cout << "run\n";
return i;
}
int eat(int i) const {
cout << "eat\n";
return i;
}
int sleep(int i) const {
cout << "ZZZ\n";
return i;
}
typedef int (Dog::*PMF) (int) const;
class FunctionObject {
Dog* ptr;
PMF pmem;
public:
FunctionObject(Dog* wp, PMF pmf)
: ptr(wp), pmem(pmf) {
cout << "FunctionObject constructor\n";
}
int operator()(int i) const {
cout << "FunctionObject::operator()\n";
return (ptr->*pmem)(i); // Make the call
}
};
FunctionObject operator->*(PMF pmf) {
cout << "operator->*" << endl;
return FunctionObject(this, pmf);
}
};
Unusual Operators
int main() {
Dog w;
Dog::PMF pmf = &Dog::run;
cout << (w->*pmf)(1) << endl;
pmf = &Dog::sleep;
cout << (w->*pmf)(2) << endl;
pmf = &Dog::eat;
cout << (w->*pmf)(3) << endl;
} ///:~
Other Operators
You cannot overload the following operators:
● operator. - is used to access ANY member of a class -overwriting it may prevent it from being used to access some members
● operator.* - same reasons● operator** - C doesn’t have exponentiation, and it’s difficult
to parse
You also can’t make up your own operators or change precedence rules
Non-member Operators
Generally, you should use member function for operators to emphasize the association between the operators and its class
● What if we want the left-hand operand to be an object of ANOTHER class?● You have no choice but to define a non-member operator● Common use case is for << and >> overloaded for iostream
Recommended Usage:
All unary member
= () [] -> ->* MUST be member
+= -= /= … member
All other binary operators non-member
ostream&
operator<<(ostream& os, constIntArray& ia) {
for(int j = 0; j < ia.sz; j++) {
os << ia.i[j];
if(j != ia.sz -1)
os << ", ";
}
os << endl;
return os;
}
istream& operator>>(istream& is, IntArray& ia){
for(int j = 0; j < ia.sz; j++)
is >> ia.i[j];
return is;
}
class IntArray {
enum { sz = 5 };
int i[sz];
public:
IntArray() { memset(i, 0, sz* sizeof(*i)); }
int& operator[](int x) {
require(x >= 0 && x < sz,
"IntArray::operator[] out of range"); // range checking
return i[x];
}
friend ostream&
operator<<(ostream& os, constIntArray& ia);
friend istream&
operator>>(istream& is, IntArray& ia);
};
Example
Example
int main() {
stringstream input("47 34 56 92 103");
IntArray I;
input >> I;
I[4] = -1; // Use overloaded operator[]
cout << I;
} ///:~
More on =
MyType b;
MyType a = b;
a = b;
What happens for line 2?
● Remember that C++ will initialize the object when it is defined (but memory allocated at beginning of its scope)
● Is it using the regular constructor or the copy-constructor (since it’s defined as equal to b) - it uses the copy constructor since it’s being created for the first time
For line 3,
● regular operator= is called (since a already exists)
Avoid using = for initialization an object (use the constructor, if at all possible)
● Any time an object is initialized with “=” (instead of the ordinary constructor form), the compiler will look for a constructor that accepts the RHS operand of “=” (e.g., copy-constructor)
class Fi {
public:
Fi() {}
};
class Fee {
public:
Fee(int) {}
Fee(const Fi&) {}
};
int main() {
Fee fee = 1; // actually Fee(int) is called
Fi fi;
Fee fum = fi; // Fee(Fi)
}
Example
More on =When you create a operator= make sure to copy all necessary information from the RHS object into the current object
Return the reference, so more complex expressions can be created
Always check for self-assignment
class Value {
int a, b;
float c;
public:
Value(int aa = 0, int bb = 0, float cc = 0.0)
: a(aa), b(bb), c(cc) {}
Value& operator=(const Value& rv) {
a = rv.a;
b = rv.b;
c = rv.c;
return *this;
}
friend ostream&
operator<<(ostream& os, const Value& rv) {
return os << "a = " << rv.a << ", b = "
<< rv.b << ", c = " << rv.c;
} };
Example
int main() {
Value a, b(1, 2, 3.3);
cout << "a: " << a << endl;
cout << "b: " << b << endl;
a = b;
cout << "a after assignment: " << a << endl;
} ///:~
What would be printed?
More on =What about pointers in the class?
● Do I copy the just the pointer or the data pointed to by the pointer?
● This depends on usage, but simplest solution is to make another copy of the data pointed to by the pointer
Reference Counting
If your objects require a lot of resources to initialize, avoid copying it with (operator=)
Common approach to doing this is called “reference counting”
The object that’s being pointed to (instead of copied) keeps track of how many objects are pointing to it
Copying (i.e., using copy-constructor) is attaching another point to an existing object and incrementing the reference count
Destruction is reducing the reference count, and only use the destructor when the reference count goes to zero
What if you want to actually write to the object?
● copy-on-write ● If you are the only one pointing to it, go ahead and
write● If not, create a personal copy of it, then change it
Lastly
If you don’t define an operator=, the compiler creates one for you
Behavior mimics the automatically created copy-constructor - if the class contains objects, the operator= for those objects is called recursively (those may also have been automatically created)
● This is also known as member-wise assignment
Automatic Type Conversion
If the compiler sees an expression or function using a type that isn’t quite what it needs, it can often perform automatic type conversion
In C++ you can achieve the same for user-defined types (i.e., classes), by defining automatic type conversion functions, using
A particular type of constructor
Overloaded operator
Automatic Type Conversion
Constructor conversionclass One {
public:
One() {}
};
class Two {
public:
Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
f(one); // Wants a Two, has a One
} ///:~
Preventing constructor conversionclass One {
public:
One() {}
};
class Two {
public:
explicit Two(const One&) {}
};
void f(Two) {}
int main() {
One one;
//! f(one); // No auto conversion allowed
f(Two(one)); // OK -- user performs conversion
} ///:~
Automatic Type Conversion
Operator Conversionclass Three {
int i;
public:
Three(int ii = 0, int = 0) : i(ii) {}
};
class Four {
int x;
public:
Four(int xx) : x(xx) {}
operator Three() const { return Three(x); }
};
void g(Three) {}
int main() {
Four four(1);
g(four);
g(1); // Calls Three(1,0)
} ///:~
Automatic Type Conversion
class Number {
int i;
public:
Number(int ii = 0) : i(ii) {}
const Number
operator+(const Number& n) const {
return Number(i + n.i);
}
friend const Number
operator-(const Number&,
const Number&);
};
Reflexivity
With global operators conversion can be applied to either operands
● Whereas with member conversion, the LHS operand must be of the proper type
const Numberoperator-(const Number& n1,
const Number& n2) {return Number(n1.i - n2.i);
}int main() {
Number a(47), b(11);a + b; // OKa + 1; // 2nd arg converted to
Number//! 1 + a; // Wrong! 1st arg not of type Number
a - b; // OKa - 1; // 2nd arg converted to
Number1 - a; // 1st arg converted to
Number} ///:~
Pitfalls
Since the compiler “quietly” performs the conversion, if you don’t design the conversion properly, you might introduce a bug
● For example, if there are two ways to convert (constructor AND operator overload), you have ambiguous conversion
class Orange; // Class declaration
class Apple {
public:
operator Orange() const; // Convert Apple to Orange
};
class Orange {
public:
Orange(Apple); // Convert Apple to Orange
};
void f(Orange) {}
int main() {
Apple a;
//! f(a); // Error: ambiguous conversion
} ///:~