COMP6771 Week 4.1

Operator Overloading

Start with an example

  • Line 26 is our best attempt to "Add two points together and print them"

 

print(std::cout, Point::add(p1, p2));

 

  • This is clumsy and ugly. We'd much prefer to have a semantic like this

 

std::cout << p1 + p2;

#include <iostream>

class Point {
  public:
    Point(int x, int y) : x_{x}, y_{y} {};
    const int& x() const { return this->x_; };
    const int& y() const { return this->y_; };
    static Point add(const Point& p1, const Point& p2);
  private:
    int x_;
    int y_;
};

void print(std::ostream& os, const Point& p) {
  os << "(" << p.x() << "," << p.y() << ")";
}

Point Point::add(const Point& p1, const Point& p2) {
  return Point{p1.x() + p2.x(), p1.y() + p2.y()};
}

int main() {
  Point p1{1, 2};
  Point p2{2, 3};
  print(std::cout, Point::add(p1, p2));
  std::cout << "\n";	
}

Start with an example

#include <iostream>
#include <ostream>

class Point {
  public:
    Point(int x, int y) : x_{x}, y_{y} {};
    const int& x() const { return this->x_; };
    const int& y() const { return this->y_; };
    static Point add(const Point& p1, const Point& p2);
  private:
    int x_;
    int y_;
};

void print(std::ostream& os, const Point& p) {
  os << "(" << p.x() << "," << p.y() << ")";
}

Point Point::add(const Point& p1, const Point& p2) {
  return Point{p1.x() + p2.x(), p1.y() + p2.y()};
}

int main() {
  Point p1{1, 2};
  Point p2{2, 3};
  print(std::cout, Point::add(p1, p2));
  std::cout << "\n";	
}
#include <iostream>
#include <ostream>

class Point {
  public:
    Point(int x, int y) : x_{x}, y_{y} {};
    friend Point operator+(const Point& lhs, const Point& rhs);
    friend std::ostream& operator<<(std::ostream& os,
                                    const Point& p);
  private:
    int x_;
    int y_;
};

Point operator+(const Point& lhs, const Point& rhs) {
  return Point{lhs.x_ + rhs.x_, lhs.y_ + rhs.y_};
}

std::ostream& operator<<(std::ostream& os, const Point& p) {
  os << "(" << p.x_ << "," << p.y_ << ")";
  return os;
}

int main() {
  Point p1{1, 2};
  Point p2{2, 3};
  std::cout << p1 + p2 << "\n";	
}

Operator Overloading

  • C++ supports a rich set of operator overloads
  • All operator overloads must have at least one operand of its type
  • Advantages:
    • Reuse existing code semantics
    • No verbosity required for simple operations
  • Disadvantages:
    • Lack of context on operations
  • Only create an overload if your type has a single, obvious meaning to an operator

Operator Overload Design

Type Operator(s) Member / friend
I/O <<, >> friend
Arithmetic +, -, *, / friend
Relational, Equality >, <, >=, <=, ==, != friend
Assignment = member (non-const)
Compound assignment +=, -=, *=, /= member (non-const)
Subscript [] member (both)
Increment/Decrement ++, -- member (non-const)
Arrow, Deference ->, * member (both)
Call () member
  • Use members when the operation is called in the context of a particular instance
  • Use friends when the operation is called without any particular instance
    • Even if they don't require access to private details

Overload: I/O

// Point.h:
#include <ostream>
#include <istream>
class Point {
  public:
    Point(int x, int y) : x_{x}, y_{y} {};
    friend std::ostream& operator<<(std::ostream& os, const Point& type); 
    friend std::istream& operator>>(std::istream& is, Point& type); 

  private:
    int x_;
    int y_;
};

// Point.cpp:
#include <ostream>
#include <istream>
#include <oistream>
std::ostream& operator<<(std::ostream& os, const Point& p) {
  os << "(" << p.x_ << "," << p.y_ << ")";
  return os;
}
std::istream& operator>>(std::istream& is, Point& type) {
  // To be done in tutorials
}

int main() {
  Point p{1,2};
  std::cout << p << '\n';
}

Overload: Compound assignment

// Point.h:
class Point {
  public:
    Point& operator+=(const Point& p);
    Point& operator-=(const Point& p);
    Point& operator*=(const Point& p);
    Point& operator/=(const Point& p);
    Point& operator*=(const int& i);

  private:
    int x_;
    int y_;
};

// Point.cpp:
Point& Point::operator+=(const Point& p) {
  this->x_ += p.x_;
  this->y_ += p.y_;
  return *this;
}
Point& Point::operator-=(const Point& p) { /* Should we do this one? */ }
Point& Point::operator*=(const Point& p) { /* Should we do this one? */ }
Point& Point::operator/=(const Point& p) { /* Should we do this one? */ }
Point& Point::operator*=(const int& p) { /* Should we do this one? */ }

Overload: Relational & Equality

// Point.h:
class Point {
  public:
    // hidden friend - preferred
    friend bool operator==(const Point& p1, const Point& p2) {
      return p1.x_ == p2.x_ && p1.y_ == p2.y_;
      // return std::tie(p1.x_, p1.y_) == std::tie(p2.x_, p2.y_);
    }
    friend bool operator!=(const Point& p1, const Point& p2) {
      return !(p1 == p2);
    }
    friend bool operator<(const Point& p1, const Point& p2) {
      // Do we want this? Alternatives?
    }
    friend bool operator<=(const Point& p1, const Point& p2);
    friend bool operator>(const Point& p1, const Point& p2);
    friend bool operator>=(const Point& p1, const Point& p2);

  private:
    int x_;
    int y_;
};

Overload: Assignment

// Point.h:
#include <istream>
class Point {
  public:
    Point& operator=(const Point& p);
    Point& operator=(std::istream &is);

  private:
    int x_;
    int y_;
};

// Point.cpp:
#include <istream>
Point& Point::operator=(const Point& p) {
  this->x_ = p.x_;
  this->y_ = p.y_;
  return *this;
}
Point& Point::operator=(std::istream &is) {
  // etc
}

Overload: Subscript

// Point.h:
class Point {
  public:
    int& operator[](int i);       // setting via []
    int  operator[](int i) const; // getting via []

  private:
    int x_;
    int y_;
};

// Point.cpp:
#include <cassert>
int& Point::operator[](int i) {
  assert(i == 0 || i == 1);
  if (i == 0) return this->x_;
  else return this->y_;
};
int Point::operator[](int i) const {
  assert(i == 0 || i == 1);
  if (i == 0) return this->x_;
  else return this->y_;
};
  • Usually only defined on indexable containers
  • Different operator for get/set
  • Asserts are the right approach here as preconditions:
    • In other containers (e.g. vector), invalid index access is undefined behaviour. Usually an explicit crash is better than undefined behaviour
    • Asserts are stripped out of optimisation builds

Overload: Increment/Decrement

// RoadPosition.h:
class RoadPosition {
  public:
    RoadPosition(int km) : km_from_sydney_(km) {}
    RoadPosition& operator++();      // prefix
    // This is *always* an int, no
    // matter your type.
    RoadPosition operator++(int);   // postfix
    void tick();
    int km() { return km_from_sydney_; }

  private:
    void tick_();
    int km_from_sydney_;
};

// RoadPosition.cpp:
#include <iostream>
RoadPosition& RoadPosition::operator++() {
  this->tick_();
  return *this;
}
RoadPosition RoadPosition::operator++(int) {
  RoadPosition rp = *this;
  this->tick_();
  return rp;
}
void RoadPosition::tick_() {
  ++(this->km_from_sydney_);
} 
  • prefix: ++x, --x, returns lvalue reference
  • postfix: x++, x--, returns rvalue
  • Performance: prefix > postfix
  • Different operator for get/set
  • Postfix operator takes in an int
    • This is not to be used
    • It is only for function matching
    • Don't name the variable
int main() {
  RoadPosition rp{5};
  std::cout << rp.km() << '\n';
  int val1 = (rp++).km();
  int val2 = (++rp).km();
  std::cout << val1 << '\n';
  std::cout << val2 << '\n';
}

Overload: Arrow & Dereferencing

#include <iostream>
class StringPtr {
  public:
    StringPtr(std::string *p) : ptr{p} { }
    ~StringPtr() { delete ptr; }
    std::string* operator->() { return ptr; }
    std::string& operator*() { return *ptr; }
  private:
    std::string *ptr;
};

int main() {
  std::string *ps = new std::string{"smart pointer"};
  StringPtr p{ps};
  std::cout << *p << std::endl;
  std::cout << p->size() << std::endl;
}
  • Classes exhibit pointer-like behaviour when -> is overloaded
  • For -> to work it must return a pointer to a class type or an object of a class type that defines its own -> operator
  • Interesting example: std::optional

Overload: Other

// Point.h:
#include <vector>
class Point {
  public:
    Point(int x, int y) : x_(x), y_(y) {}
    operator std::vector<int>() {
      std::vector<int> vec;
      vec.push_back(x_);
      vec.push_back(y_);
      return vec;
    }

  private:
    int x_;
    int y_;
};

// Point.cpp:
#include <iostream>
#include <vector>
int main() {
  Point p{1,2};
  std::vector<int> vec = static_cast<std::vector<int>>(p);
  std::cout << vec[0] << '\n';
  std::cout << vec[1] << '\n';
}

COMP6771 19T2 - 4.1 - Operator Overloading

By cs6771

COMP6771 19T2 - 4.1 - Operator Overloading

  • 840