Object-Oriented Programming

CSCI-UA.0470-001

NYU, Undergraduate Division, Computer Science Course - Fall 2013

OOP Class Notes for 10/31/13

Operator Overloading

  • Java does not support operator overloading.
  • C++ does support operator overloading and we will be using this feature in our translator.
    • Why can operators be overloaded in C++?
      • Because operators are treated as methods in C++ (i.e., you can think of x + y as x::operator+(y)).
    • Advantages for operator overloading:
      • flexibility
      • convenience (e.g. matrix additions, subtractions, multiplications and inversions)
      • simplicity
      • concision
    • Disadvantages:
      • may be confusing if an operator is overloaded for multiple classes
        • Same operator symbol would represent different things
        • Makes code harder to read and debug
      • operators could be overloaded to do something unintuitive (switching + and -)

Operator overloading in translator

The array subscript operator []

So far in our translator, we need to check to see if the index is within the bounds of the array. Then we can access a specific element in the array

__rt::checkIndex(a, 2);
std::cout << "a[2]  : " << a->__data[2] << std::endl;

What if we can check if an index is within the bounds of the array while accessing the element in the array. Just like in Java. We can overload the array subscript operator to provide this additional functionality.

T& operator[](int32_t index) {
if (0 > index || index >= length) throw ArrayIndexOutOfBoundsException();  // check index
return __data[index];
}

So now we can access an element in an array by using the syntax

(*a)[2];
  • the [] operator must return a reference to an array element to support modification of the array
  • the [] operator always takes an index, so it can check whether the index is in bounds

By convention, when you define the [] operator, you also need to define a const [] operator. The const [] operator does not allow modification of a const array. If the array is const it will return a const element.

const T& operator[](int32_t index) const {
  if (0 > index || index >= length) throw ArrayIndexOutOfBoundsException();  // implementation is exactly the same
  return __data[index];
}
  • If the array is const you get a const element back
  • If the array is not const you do not get a const element back (allowing the array access to appear on the left hand side of assignments)

The left-shift operator <<

  • Every time we want to print a string, we have to use the code:
    cout << k->__vptr->getName(k)->data // k.getName()
  • Wouldn't it be nice if we didn't have to use the ->data every time we were printing our string object
    • We can do that by overloading the <<operator (left shift operator) in our code.
    • The problem is that we cannot (and we shouldn't) edit the standard library
      • Since operators are just like methods, they are overloadable.

  • We can overload the left-shift operator by writing this code
  • std::ostream& operator<<(std::ostream& out, String s){
      out << s->data;
      return out;
    }
  • This allows us to overload the left-shift operator for the ostream when the String object is passed in.
  • Returning out enables chaining. If we had void return type, then chaining would be impossible.
  • This overloads the injection operator without modifying the standard libraries
  • This prints the string with less notation in the tranlation.
  • Overloading the [] operator is an example of an operator that is a method of a class, the reciever is an instance of that class.
  • Overloading the << operator is an example of overloading an operator that is not a member of a class.

A Wrapper Class for Pointers

  • To illustrate the power of operator overloading we implement a wrapper class for the built-in pointers of C++
  • For now, the wrapper class provides the same functionaly as a regular pointer. However, later on we will extend it with functionality for automatic memory management (smart pointers).
    • We made a new template class: Ptr<T> where T is some type.
      • Ptr<int> syntax is more intuitive than int*
    • A Ptr instance contains an address to a T since we want it to behave like a real pointer
    • Constructor: Stores the address
      Ptr(T* addr): addr(addr) {
        TRACE("constructor");
      }
    • Copy Constructor: Makes a copy of an existing instance
      Ptr(const Ptr& other) : addr(other.addr) {
        TRACE("copy constructor");
      }
      • The copy constructor is used when we initilize an instance of a class with another instance
    • Destructor: Gets called when the instance is deallocated.
      ~Ptr() {
       TRACE("destructor");
      }
      • If the instance is allocated on the stack, the destructor is called automatically when the instance goes out of scope.
      • If the instance is allocated on the heap, the destructor is called when the instance is explicitly deallocated with a delete statement.
      • The destructor always implicitly calls the destructor of the super class (assuming there is one) as well as the destructors of all members which have them. If you do not provide a destructor, the compiler provides a default destructor, which does nothing accept for the above implicit destructor calls.
    • Overloading the assignment operator (=)
      operator=(const Ptr& right) {
        TRACE("assignment operator");
        if (addr != right.addr){
          addr = right.addr;
        }
        return *this;
      }
      • We overload the assignment operator because we want to make sure that the stored address in the instance is assigned.
      • When overloading the assignment operator, you MUST protect against self assignment.
    • Overloading the dereference operator (*)
      T& operator*() const {
        TRACE("dereference operator");
        return *addr;
      }
    • Overloading the arrow operator (->)
      T* operator->() const {
        TRACE("arrow operator");
        return addr;
      }