Operator Overloading Lecture 8 Operator Overloading C++ feature that allows implementer-defined classes to specify class-specific function for operators Benefits allows classes to provide natural semantics for operators in a manner similar to built-in types Drawbacks poor use leads to confusing code many classes do not have natural overloaded semantics and, thus, overloaded operators are ambiguous it is not clear that this situation is worse than with poorly named functions however, it was serious enough that Java left the feature out Running Example: Complex Numbers Syntax Complex numbers natural class for operator overloading operators against this class are well-defined conform with user expectation thus, they are easy to use class complex public:... private: double _real; double _imag; ; Operators are treated like functions take arguments, allow implementer to define semantics Operators are invoked differently than functions unary operators (take one argument) usage: -a, *b binary operators usage: a + b, c <= d Mapping operators to functions usage: a + b matches prototypes complex & complex::operator+ ( complex & other ) Syntax -- First Example What can be overloaded operator+ -- the first example The following are equivalent note that the compiler will factor out the temporary variables I prefer the first actually (aside) don t be afraid to use temporary variables when they simplify code complex & complex::operator+ ( const complex & other ) const double real = this._real + other._real; double imag = this._imag + other._imag; return complex ( real, imag ); // Simplified complex & complex::operator+ ( const complex & other ) const return complex ( _real + other._real, _imag + other._imag ); Almost all operators Arithmetic +, -, *, /, %, ++, -- Bit-wise operators ^, &, >>, << Logical operators &&,,! Comparison >, <, <=, >=, ==,!= Array, function, comma [], (),,, ->, ->* Assignment and composite operators =, ^=, =, &=, +=, -=, *=, /=, %=, <<=, >>=, 1
What cannot be done const and Some operators cannot be overloaded Non-pointer access to class members.,.* Scope :: If/then/else operator?: New operators cannot be created, even if desired x y := x y = x ** 2 Almost all overloaded operators (except those involving equality) should be const, i.e. they should not change the implicit argument similarly, they should use const function arguments complex & complex::operator+ ( const complex & other ) const return complex ( _real + other._real, _imag + other._imag ); complex a ( 1, 2); complex b ( 2, 3); complex c = a + b; // Should not change a or b Unary and Binary Anonymous Objects and Both the unary and binary forms of an operator can be overloaded differentiated by their arguments applies to: +, -, *, & complex & complex::operator- () const return complex ( -1 * _real, -1 * imag); complex & complex::operator- ( const complex & other ) const return complex ( _real - other._real, _imag - other._imag ); First (well motivated) use of anonymous objects note it is often wrong (almost always) to modify this same reason we use const // Correct complex & complex::operator- () const return complex ( -1 * _real, -1 * _imag); // Changes this complex & complex::operator- () _real *= -1; _imag *= -1; return *this; complex a ( 1, 2); complex b = -a; // The right definition of operator- // leaves a unmodified Anonymous Objects and Polymorphic Functions and First (well motivated) use of anonymous objects note it is often wrong (almost always) to modify this same reason we use const // Correct complex & complex::operator- () const return complex ( -1 * _real, -1 * _imag); // Returns reference to stack variable complex & complex::operator- () complex c; c._real = _real * -1; c._image = _imag * -1; return c; Operator overloading is done through functions, and, thus, may be polymorphic actually, unary and binary forms are already polymorphic complex & complex::operator- ( const double a ) const return complex ( _real - a ); inumber & inumber::operator- ( inumber & other ) const return complex ( _real - other._real, _imag - other._imag ); complex a ( 1, 2); complex b = -a; // The right definition of operator- // leaves a unmodified 2
Class Arguments and Global Scope Common Overloaded Operators One argument to a operator overloaded function must be a class argument i.e. it is not possible to change the implementation of operators against built-in types Operators may also be of global scope the following are equivalent note that the second is not a class member It is preferable to have overloaded operators belong to classes. Why? Now let s construct some common operators for the complex class operator<< and operator>> operator= operator< Interestingly, all of these common cases have interesting effects that do not conform well to the operator overloading model complex & complex::operator+ ( const complex & other ) const; complex & operator+ ( const complex & other1, const complex & other2 ) const; operator>> and operator<< operator>> and operator<< So we want to print out our imaginary numbers What is the prototype of the function? Is it binary or unary? What are the arguments? xxx operator<< ( const yyy, const zzz ); xxx operator<< ( const yyy ); Two possible prototypes class scope global scope What is the problem with the first prototype? Can we make the operator a member of class complex? complex a ( 1.0, 1.0); cout << a << endl; // prints 1.0 + 1.0i ostream & ostream::operator<< ( const complex & other ); ostream & operator<< ( ostream& os, const complex & other ); Some properties of ostream operator>> and operator<< Main ostream operator ostream & ostream::operator<< ( const char [] ) Also provides for all basic types ostream & ostream::operator<< ( const type & other ); Because it returns an ostream reference, the arguments maybe chained cout << The value of PI is approximately << 3.14159 << endl; Note that the function is not constant changes the state of the ostream object Implementation of the operator must choose the operator to be global scope we cannot modify class ostream ostream & operator<< ( ostream& os, const complex & other ) 3
operator>> and operator<< operator< Implementation of the operator must choose the operator to be global scope we cannot modify class ostream How did we do? All arguments for operator<< apply to operator>> just for class istream ostream & operator<< ( ostream& os, const complex & other ) os << other._real << + << other._imag << i ; return os; An obvious operator with an (usually) obvious implementation complex numbers are not well-ordered mathematically speaking Different example -- countdown timer class CDT bool CDT::operator< ( const CDT & other ) const return ( _seconds < other.seconds ) operator< operator< The catch, when one defines operator< The compiler also creates operator> ( a < b can be used to construct b > a ) operator<= (!( a < b ) implies a <= b ) operator>= (!( b < a ) implies a >= b ) What is the danger here? The catch, when one defines operator< The compiler also creates operator> ( use a < b, but switch arguments b > a ) operator<= (!( a < b ) implies a <= b ) operator>= (!( b < a ) implies a >= b ) What is the danger here? if operator< does not provide a total ordering, then the other operators will not be well defined one good example is that of line segments operator< resolves the segment based on length any two segments of the same length will be declared equal operator= Type Conversions The C++ compiler always provides an overloaded operator= even when not requested provides a byte-wise copy of the object deep copy of built in types shallow copy of pointers Every class should provide its own operator= avoids confusing/unwanted default behavior frequently a simple function often identical to the default We will discuss shallow and deep copying tomorrow Constructors are the preferred method of converting the type of an object e.g. the following converts a double to a complex let s write it complex::complex ( double real ) _real = real; _imag = 0; complex & operator= ( const complex & other ) this._real = other._real; this._imag = other._imag; 4
Type Conversions Type Conversions Constructors are the preferred method of converting the type of an object e.g. the following converts a double to a complex how did we do? complex::complex ( double real ) : _real ( real ), _imag ( 0 ) Constructors are the preferred method of converting the type of an object Why are they the preferred technique? What is the alternative? Any constructor that accepts a single argument doubles as a type conversion routine Two circumstances under which we cannot perform type conversion via constructors they are? Two circumstances under which we cannot perform type conversion via constructors when the target type is not a class when the target type is a class that the implementer cannot modify Two circumstances under which we cannot perform type conversion via constructors when the target type is not a class when the target type is a class that the implementer cannot modify Type conversion can be performed with operators For example, if we want to construct a string out of complex Some observations we cannot do this by providing a new constructor in string this would be preferred, but we cannot change string operator string() does not have strict function semantics does not specify a return type complex::operator string () const char buffer [21]; sprintf ( buffer, %d + %di, _real, _imag ); return string ( buffer ); complex::operator string() const char buffer [21]; sprintf ( buffer, %d + %di, _real, _imag ); return string ( buffer ); 5
operator[] Questions Frequently overloaded in container classes C++ standard class vector overloads to provide array-like behavior Simple example -- look at the contents of a stack of items Rarely a good idea to overload unless the class is essentially a semantically-rich array Why can one overload the operators that access class members through pointers (->, ->*), while it is illegal to overload standard class access members (.,.*)? class stack public: item & operator[] ( int which ) const; private: vector <item> v; ; item & operator[] ( int which ) const if (( which < v.length() ) && ( which >= 0 )) return v[which]; Why/When/What to not Overload Precedence confusing precedence expected equivalence Benefit of operators is operator-like syntax, rather than functional syntax convenient, intuitive and concise,... right? but, why are there RPN calculators? Operator evaluation relies on evaluation ordering rules C++ is not the same as mathematics for example, ^ is the C++ bitwise exclusive-or (not exponentiation) overloading for exponentiation does not work a ^ 2 + 1 evaluates to a.operator^(2+1) math would prefer a.operator^(2) + 1 Functions do not have precedence problems functional notation (parentheses) ensure this Equivalence Operations Why no operator overloading in Java We have an expectation that the following are the same: a = a + 1; a++; ++a; a += 1; However, they invoke four entirely different functions It is the responsibility of the implementer to provide a complete and logically consistent set of operators frequently a tedious (and often incomplete in my experience) task Book gives a lot of guidelines on operator overloading merits and demerits dangers of usage Why would Java leave this (well-established) feature out of a very similar OO language? Do you agree with the decision? What key C++ feature relies on operator overloading? interestingly, Java is weak in this department 6
How bad does it get? How bad does it get? Frequently, for error testing, it is desirable to overload class::operator bool( ) Used to detect errors ostream::operator bool( ) returns true if an error on the stream, false otherwise However, for compatibility reasons, bool can be coerced (by the compiler) automatically to int. consider the code snippet below it would compile and attempt to shift cout x bits to the right cout << x << y << z << endl; if ( cout ) // process error cout >> x; if ( cout ) // process error // probably meant cout << x How do we solve this? Beware of Compiler Coercion Overloading class::operator void* () solves the problem void* cannot be shifted or manipulated by other operators stupid idiom to address a stupid problem If a compiler cannot find an exact match for a function prototype that you have provided, it will find a near match by coercion our example, converting bool to int This is dangerous because often it is unintended running the compiler with -Wall will let the implementer know when all such implicit coercions occur this is a very good idea cout >> x; if ( cout ) // process error // this now hurls an error as cout cannot // be coerced to a integer int x = 1; int y = -7; complex a ( x, y ); // invokes complex ( double, double ) // not so dangerous, safe-ish coercion double x = 54.2; CoundownTimer cdt ( x ); // invokes CountdownTimer ( int ) // losing the.2 seconds 7