Example ADT - Rational Numbers

  • Sane default value for this type?
    • 0 is pretty reasonable
  • Should be math ops, and they should use + / *, etc...
  • Should be comparison ops, and they should use ==, !=, <=, etc...
  • Should be I/O operators, to output the ADT
  • Should have copy constructor, and can be copied with = operator
//*****************************************
// Main Program
//*****************************************


int main () {
    Rational r, s;

    cout << "Enter rational number (a/b): ";
    cin >> r;
    cout << "Enter rational number (a/b): ";
    cin >> s;

    Rational t = r;

    r = r + s;

    cout << "r = " << r << endl;
    cout << "s = " << s << endl;
    cout << "t = " << t << endl;

    return 0;
}
class Rational {
public:
    Rational (); // === 0/1
    Rational (int num, int denom) throw (char const*);
    explicit Rational (int num) // == num/1
private:
    int numerator_;
    int denominator_;
}

A constructor initializes new objects to a legal value;

Q: How to deal with illegal values?
A: Until we get to exception handling, just spit out an error message, maybe set to a sane value, heck, maybe even kill the program. Return codes can also work (not on ctors though)

2. Public Accessors and Mutators

class Rational {
public:
    int numerator() const;
    int denominator() const;
    void numeratorIs(const int);
    void denominatorIs(const int); throw (char const*)
}

accessors and mutators provide restricted read/update access to data members.

Best Practice: Mutators should check that the client provided values are within ADT value range

Best Practice: Whenever possible, pass params by const reference, and use const member functions. Let the compiler help you avoid silly things!

3. Function Overloading

Function Overloading allows you to use the same function name for variants of the same function.

  • functions must have different argument signatures
  • cannot overload functions that differ only by return type

4. Default Arguments

class Rational {
public:
    Rational (int num = 0, int denom = 1);
}

Use default arguments to combine variants that vary in user provided arguments.

  • must appear only in the function declaration
  • only trailing parameters may have default values
  • once one default argument is used in a function call, all subsequent arguments in call must be defaults

5. Operator Overloading

Rational Rational::operator+ ( const Rational &r ) const {
    return Rational( numerator() * r.denominator() + denominator() * r.nominator(),
           denominator() * r.denominator() );
}

bool Rational::operator== ( const Rational &r ) const {
    Rational a( this->reduce() );
    Rational b( r->reduce() );

    return ( (a.numerator()==b.numerator()) && (a.denominator()==b.denominator() );
}

Best Practice: use operator signatures that the client programmer is used to (e.g., operator== returns a bool, operator+ adds, not multiplies) C++ notes:

  • Cannot create new operations (e.g., operator**)
  • Cannot change the order of operator evaluations
  • Cannot change the number of arguments

As a programmer implementing operator overloading, you are at liberty to choose:

  • return types
  • argument types (well, within limits)
  • whether to pass by value or by reference
  • const or not
  • member or not (once again, within limits)

6. Non Member Functions

class Rational {
…
};

// Arithmetic Operations
Rational operator+ (const Rational&, const Rational&);
Rational operator* (const Rational&, const Rational&);

// Comparison Operations
bool operator== (const Rational&, const Rational&);
bool operator!= (const Rational&, const Rational&);

A non-member function is a critical function of the ADT that is declared outside of the class.

  • Reduces number of functions with direct access to private data members
  • Some functions have to be non-member functions (e.g., operator>>)

Downside: These functions lose access to an object's private information, as they are outside of the class

  • can make them friends of the class, but that could get mucky
  • best to just write a solid suite of public functions, and use those.

operator>>, operator<<

class Rational {
    friend ostream& operator<< (ostream&, const Rational&);
    friend istream& operator>> (istream&,       Rational&);
    ...
};

ostream& operator<< (ostream &sout, const Rational &r);
istream& operator>> (istream &sin,        Rational &s);

...

// example client code:
cout << r << " + " << s << " = " << r+s << endl;

Best Practice: Streaming operators should be nonmember functions, so that first operand is reference to stream object.

Best Practice: Return value is modified stream, so that stream operations can be chained.

A note on what can and cannot be a method:

Q: What methods have to be members?

  • accessors and mutators
  • constructors, destructors
  • operator[] (indexing into the object)
  • virtual methods

Q: What methods cannot be members?

  • I/O operators
    • more generally, any operation where we don't want the first argument to be the class type. (eg: add int to rational num)

Generally though, prefer to make non-member, non-friend functions

  • less code is affected if an implementation changes, since functions don't know about internals (only use accessors and mutators)

7. Type Conversion of ADT object

Q: How does a compiler find a conversion function?

  1. try to find an exact match (int -> int)
  2. lossless conversion via "promotion" (int -> long int, float -> double)
  3. standard conversions (double -> int, int -> double)
  4. user defined conversions

If there are 2 (or more) matches at the same level, the choice is ambiguous, so the compiler will throw an error.

Otherwise, the compiler will try to find the best match on an argument by argument basis.

Example:

// -- definitions -- //
class X { public: X(int); X(string) }
class Y { public: Y(int); }
class Z { public: Z(X); }

X f(X);
Y f(Y); // overloaded function

Z g(Z);

X operator+(X, X);

// -- example code -- //
string s {"mack"};

f(X(1));      // OK
f(1);         // BAD: ambiguous (X and Y both have (int) constructors)
g(X(s))       // OK since X(s) -> Z
g(Z(s))       // OK since s -> X(s) -> Z
g(s)          // BAD: far too many steps to figure out itself
X x1 = 1 + 1; // OK since 1 + 1 -> X(1) + X(1) -> operator exists

The compiler uses constructors that have one argument to perform implicit type conversion.

Also true of constructors that have more than one argument, if rest of arguments have default values.

Can prohibit this use of constructors via explicit keyword

See the slides for the last three...

results matching ""

    No results matching ""