CIS-255 Home http://www.c-jump.com/bcc/c255c/c255syllabus.htm
A simple mechanism for improving program readability...
...or for making programs completely unreadable!
Syntactic sugar replacing function calls.
The idea is to allow operator syntax for objects:
Point p1;
Point p2;
//...
if ( p1 == p2 ) {
//...
}
Numerical: +, -, *, ++, %, etc.
Comparison: >, >=, ==, !=, etc.
Structural: [], *, &, etc.
One operator is more important than the others:
The assignment operator is provided (at no cost) if you don't provide one yourself...
...it might not be what you want.
Default assignment does assignment of all member objects, and bitwise copy of all native types.
Objects containing addresses (pointers) will be assigned shallowly.
Read more about object copying in Wikipedia.
Operators are functions.
Operators are (almost) always unary or binary.
Operators can be members, or regular functions.
How many arguments does an operator take?
Unary | Binary | |
---|---|---|
Member | 0 | 1 |
Non-member | 1 | 2 |
It's an error to declare an operator with the wrong number of arguments.
A rational number is a number that can be expressed as a fraction
n / d
where n and d are integers and d != 0.
Thus, a rational number n/d is said to have numerator n and denominator d.
Denominator is usually kept strictly positive.
Rational numbers can simplified (normalized or reduced) in case when the numerator and denominator has one or more common factors.
For example,
Rational(5,6) represents 5/6
Rational(5) represents 5/1
Rational numbers can be reduced to their lowest terms:
Rational(6,10) can be reduced to 3/5
Division by zero is obviously not allowed:
Rational(3,0) yields Zero Division Error at runtime.
Rational numbers are used excessively in geometry.
To yield a floating point approximation of a rational, a method
double fraction = Rational(1,3).to_double();
could be used.
For more information, see The Rational Class of the GNU C++ Library programming tools.
// rational.h class Rational { private: int m_n; int m_d; public: // Member binary operator bool operator==( Rational const& other) const; }; // rational.cpp bool Rational::operator==( Rational const& other ) const { // This way, 1/2 == 2/4 (should it?) return ( ( m_n * other.m_d ) == ( m_d * other.m_n ) ); }
// rational.h class Rational { //... public: // Unary negation, as in: x = -y Rational operator-() const; }; // rational.cpp Rational Rational::operator-() const { // uses 2-argument constructor return Rational( -m_n, m_d ); }
// rational.h #include <iostream> using namespace std; class Rational { friend ostream& operator<<( ostream& os, Rational const& rat ); //... }; // rational.cpp ostream& operator<<( ostream& os, Rational const& rat ) { os << "(" << rat.m_n << "/" << rat.m_d << ")"; return os; } // main.cpp #include <iostream> #include "rational.h" using namespace std; int main( ) { Rational r1; cout << r1 << endl; return 0; }
Binary arithmetic: + - * / % Unary arithmetic: + - Binary bitwise: & ^ | Unary bitwise: ~ Binary logical: && || Unary logical: ! Shift (and stream): << >> Comparison (all binary): == != > < >= <= Assignment: = += -= *= /= %= &= ^= |= >>= <<= Increment (two of each): ++, -- Binary structure: [] Unary structure: * -> Miscellaneous: , () new delete new[] delete[]
See also: C++ Operator Precedence Chart
These operators must be members:
= [] () and ->
This ensures the first arguments are lvalues (see next slide).
These operators are predefined (i.e. you get them for free):
operator= operator& operator, (comma)
The following operators cannot be overloaded:
:: (scope) . (member selector) .* (member selection through pointer to member)
Non-member operators must take at least one user-defined type.
lvalue is an object that refers to a region of storage in memory that can be both examined and stored into.
However, lvalue does not necessarily permit modification of the object it designates.
Thus, lvalue can be
modifiable lvalue
non-modifiable lvalue
For example, a function call that returns a reference is an lvalue.
Certain operators require lvalues:
&X // Unary address-of operator requires that operand X must be an lvalue. X++ --X // Operand X must be an lvalue. This applies to both prefix and postfix forms. = += -= *= %= <<= >>= &= ^= |= // Left operand must be an lvalue.
The term rvalue refers to a data value that is stored at some address in memory.
An rvalue cannot have a new value assigned to it, for example,
literal constant "Hello" or a const variable.
When necessary, an lvalue is implicitly converted to an rvalue, but the reverse, however, is not true: an rvalue cannot be converted to an lvalue.
Should return *this (by reference), to allow for assignment chaining.
Almost always needs to check for self assignment condition:
Rational& Rational::operator=( Rational const& other ) { if ( this == &other ) // beware of self assignment! return *this; m_n = other.m_n; m_d = other.m_d; return *this; }
If you have a copy constructor, you should have an operator=
Once you have operator=, a copy constructor is very simple:
class Rational { public: Rational( Rational const& other ); Rational& operator=( Rational const& other); //... }; Rational::Rational( Rational const& other ) { *this = other; }
Most classes need =, many need ==, some need >.
Numerical classes gain natural syntax of arithmetic operators.
String classes provide + and += for concatenation, and [ ] for character access.
Lookup structures add operator[ ] taking key types:
// PhoneBook.h class PhoneBook { public: string const& operator[]( string const& key ); //... }; // main.cpp #include <iostream> #include "PhoneBook.h" using namespace std; int main( ) { PhoneBook pb; //... cout << "Bob's number is " << pb[ "Bob" ] << endl; return 0; }
// rational.h class Rational { //... public: Rational& operator--(); // prefix (result is an lvalue!) const Rational operator--(int); // postfix (not an lvalue) }; // rational.cpp Rational& Rational::operator--() { m_n -= m_d; return *this; } const Rational Rational::operator--(int) { Rational temp = *this; m_n -= m_d; return temp; }
It is common to define two operator[ ]s
Allows use on left-hand side, and on const objects:
class MyDoubleArray { double& operator[](unsigned int i); double operator[](unsigned int i) const; //... }; void copy( MyDoubleArray& dest, MyDoubleArray const& src ) { for ( size_t idx = 0; idx < src.size(); ++idx ) { // Use non-const op[] on left, const op[] on right: dest[ idx ] = src[ idx ]; } }
Defining a constructor which takes one argument of type T gives conversion from T to your type:
// rational.h class Rational { private: int m_n; int m_d; public: Rational( int num ) // convert int to Rational : m_n(num), m_d(1) { } }; void print_rational( Rational const& rat ); // main.cpp #include "rational.h" int main( ) { Rational rat = 3; // Ok: same as r(3) print_rational(3); // Ok: one-level implicit conversion return 0; }
Sometimes we are better off without constructor-conversions:
class String { public: String( int i ) // Innocent idea: set initial size! { //... } }; String foo() { return 0; // probably a typo?! } void bar( String const& str ) { //... } int main( ) { String s1(4); // OK String s2 = 4; // Confusing String s3 = foo(); // Really?? bar(4); // Error?? return 0; }
FYI: the above compiles just fine!
Better solution: make constructor taking a single size argument explicit.
operator( ) can only be defined as a member.
Classes which define operator( ) are functors.
operator( ) can take any number of arguments
Use functors in place of function pointers (and vice versa!)
We will see examples of functors when studying the STL algorithms.
|
|
When you overload logical
operator&& operator||
operators, the operands must be evaluated, which isn't the way things normally work with short circuiting of built-in types.
This creates programmer astonishment and bugs.
This is why it is generally bad to overload these operators!
See also: Rules for C++ Programming
Use operators in ways which mimic their native use:
Don't take or return unexpected types.
Make sure you provide a complete set.
Use named functions at first, build operators on those later.
Be careful with conversion to other types.
Define assignment if you define a copy constructor.
Prefer member functions over non-members for operations that need access to the implementation.
Prefer non-member functions over members for operations that do not need access to the implementation.