COMP6771

Advanced C++ Programming

Week 3.2

Operator Overloading

Start with an example

  • Line 32 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} {};
	[[nodiscard]] int const x() const {
		return this->x_; }
	[[nodiscard]] int const y() const {
		return this->y_; }
	static point add(point const& p1, point const& p2);

private:
	int x_;
	int y_;
};

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

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

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

lecture-3/demo361-point1.cpp

Start with an example

Using operator overloading:

  • Allows us to use currently understood semantics (all of the operators!)
  • Gives us a common and simple interface to define class methods
#include <iostream>

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

private:
	int x_;
	int y_;
};

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

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

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

lecture-3/demo362-point2.cpp

Friends

  • A class may declare friend functions or classes
    • Those functions / classes are non-member functions that may access private parts of the class
    • This is, in general, a bad idea, but there are a few cases where it may be required
      • Nonmember operator overloads (will be discussing soon)
      • Related classes
        • A Window class might have WindowManager as a friend
        • A TreeNode class might have a Tree as a friend
        • Container could have iterator_t<Container> as a friend
          • Though a nested class may be more appropriate
    • Use friends when:
      • The data should not be available to everyone
      • There is a piece of code very related to this particular class

 

In general we prefer to define friends directly in the class they relate to

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 (const and non-const)
Increment/Decrement ++, -- member (non-const)
Arrow, Deference ->, * member (const and non-const)
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

#include <istream>
#include <ostream>

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_;
};

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

std::istream& operator>>(std::istream& is, point& p) {
	// To be done in tutorials
}

auto main() -> int {
	point p(1, 2);
	std::cout << p << '\n';
}

lecture-3/demo363-ui.cpp

  • Equivalent to .toString() method in Java
  • Scope to overload for different types of output and input streams

Overload: Compound assignment

class point {
public:
	point(int x, int y)
	: x_{x}
	, y_{y} {};
	point& operator+=(point const& p);
	point& operator-=(point const& p);
	point& operator*=(point const& p);
	point& operator/=(point const& p);
	point& operator*=(int i);

private:
	int x_;
	int y_;
};

point& point::operator+=(point const& p) {
	x_ += p.x_;
	y_ += p.y_;
	return *this;
}

point& operator+=(point const& p) { /* what do we put here? */}
point& operator-=(point const& p) { /* what do we put here? */}
point& operator*=(point const& p) { /* what do we put here? */}
point& operator/=(point const& p) { /* what do we put here? */}
point& operator*=(int i) { /* what do we put here? */}

lecture-3/demo354-compassign.cpp

  • Sometimes particular methods might not have any real meaning, and they should be omitted (in this case, what does dividing two points together mean).
  • Each class can have any number of operator+= operators, but there can only be one operator+=(X) where X is a type.
    • That's why in this case we have two multiplier compound assignment operators

Operator pairings

Many operators should be grouped together. This table should help you work out which are the minimal set of operators to overload for any particular operator.

If you overload

operator-(T, U)

 Then you should also overload

If you overload

operator+(T, U)
operator+(T)
operator-(T)
operator/(T, U)
operator*(T, U)
operator%(T, U)
operator/(T, U)
operator++()
operator++(int)
operator--()
operator++()
operator--(int)
operator+(T, U)
operator+(U, T)
operator->()
operator*()
operator OP=(T, U)
operator OP(T, U)

Overload: Relational & Equality

#include <iostream>

class point {
public:
	point(int x, int y)
	: x_{x}
	, y_{y} {}
	// hidden friend - preferred
	friend bool operator==(point const& p1, point const& p2) {
		return p1.x_ == p2.x_ and p1.y_ == p2.y_;
		// return std::tie(p1.x_, p1.y_) == std::tie(p2.x_, p2.y_);
	}
	friend bool operator!=(point const& p1, point const& p2) {
		return not (p1 == p2);
	}
	friend bool operator<(point const& p1, point const& p2) {
		return p1.x_ < p2.x_ and p1.y_ < p2.y_;
	}
	friend bool operator>(point const& p1, point const& p2) {
		return p2 < p1;
	}
	friend bool operator<=(point const& p1, point const& p2) {
		return not (p2 < p1);
	}
	friend bool operator>=(point const& p1, point const& p2) {
		return not (p1 < p2);
	}

private:
	int x_;
	int y_;
};

auto main() -> int {
	auto const p2 = point{1, 2};
	auto const p1 = point{1, 2};
	std::cout << "p1 == p2 " << (p1 == p2) << '\n';
	std::cout << "p1 != p2 " << (p1 != p2) << '\n';
	std::cout << "p1 < p2 " << (p1 < p2) << '\n';
	std::cout << "p1 > p2 " << (p1 > p2) << '\n';
	std::cout << "p1 <= p2 " << (p1 <= p2) << '\n';
	std::cout << "p1 >= p2 " << (p1 >= p2) << '\n';
}

lecture-3/demo355-relation1.cpp

  • Do we want all of these?
  • We're able to "piggyback" off previous definitions
  • Check out the spaceship operator

Overload: Assignment

#include <istream>

class point {
public:
	point(int x, int y)
	: x_{x}
	, y_{y} {};
	point& operator=(point const& p);

private:
	int x_;
	int y_;
};

point& point::operator=(point const& p) {
	x_ = p.x_;
	y_ = p.y_;
	return *this;
}

lecture-3/demo356-assign.h

  • Similar to compound assignment

Overload: Subscript

#include <cassert>

class point {
public:
	point(int x, int y)
	: x_{x}
	, y_{y} {};
	int& operator[](int i) {
		assert(i == 0 or i == 1);
		return i == 0 ? x_ : y_;
	}
	int operator[](int i) const {
		assert(i == 0 or i == 1);
		return i == 0 ? x_ : y_;
	}

private:
	int x_;
	int y_;
};

lecture-3/demo357-subscript.h

  • 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 optimised builds

Overload: Increment/Decrement

lecture-3/demo358-incdec.h

auto main() -> int {
  auto rp = RoadPosition(5);
  std::cout << rp.km() << '\n';
  auto val1 = (rp++).km();
  auto val2 = (++rp).km();
  std::cout << val1 << '\n';
  std::cout << val2 << '\n';
}
// 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
    • Discussed more in week 5
  • postfix: x++, x--, returns rvalue
    • Discussed more in week 5
  • 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

lecture-3/demo358-incdec.cpp

Overload: Arrow & Dereferencing

#include <iostream>
class stringptr {
public:
	explicit stringptr(std::string const& s)
	: ptr_{new std::string(s)} {}
	~stringptr() {
		delete ptr_;
	}
	std::string* operator->() const {
		return ptr_;
	}
	std::string& operator*() const {
		return *ptr_;
	}

private:
	std::string* ptr_;
};

auto main() -> int {
	auto p = stringptr("smart pointer");
	std::cout << *p << '\n';
	std::cout << p->size() << '\n';
}

lecture-3/demo359-arrow.cpp

  • This content will feature heavily in week 5
  • 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

Overload: Type Conversion

#include <vector>

class point {
public:
	point(int x, int y)
	: x_(x)
	, y_(y) {}
	explicit operator std::vector<int>() {
		std::vector<int> vec;
		vec.push_back(x_);
		vec.push_back(y_);
		return vec;
	}

private:
	int x_;
	int y_;
};

lecture-3/demo360-type.h

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

lecture-3/demo360-type.cpp

Overload: New Function Syntax

#include <iostream>
class stringptr {
public:
	explicit stringptr(std::string const& s)
	: ptr_{new std::string(s)} {}
	~stringptr() {
		delete ptr_;
	}
	auto operator->() const -> std::string* {
		return ptr_;
	}
	auto operator*() const -> std::string& {
		return *ptr_;
	}

private:
	std::string* ptr_;
};

auto main() -> int {
	auto p = stringptr("smart pointer");
	std::cout << *p << '\n';
	std::cout << p->size() << '\n';
}

lecture-3/demo361-syntax.cpp

  • We are able to use the new function syntax on our operator overloads as well

Overload: Spaceship Operator

#include <compare>
#include <iostream>

class point {
public:
    point(int x, int y)
    : x_{x}
    , y_{y} {}

    // hidden friend - preferred
    // return type deduced as std::strong_ordering
    friend auto operator<=>(point p1, point p2) = default;

private:
    int x_;
    int y_;
};

auto main() -> int {
    auto const p2 = point{1, 2};
    auto const p1 = point{1, 2};
    std::cout << "p1 == p2 " << (p1 == p2) << '\n';
    std::cout << "p1 != p2 " << (p1 != p2) << '\n';
    std::cout << "p1 < p2 " << (p1 < p2) << '\n';
    std::cout << "p1 > p2 " << (p1 > p2) << '\n';
    std::cout << "p1 <= p2 " << (p1 <= p2) << '\n';
    std::cout << "p1 >= p2 " << (p1 >= p2) << '\n';
}

lecture-3/demo355-relation2.cpp

#include <compare>
#include <iostream>

class point {
public:
    point(double x, double y)
    : x_{x}
    , y_{y} {}

    // hidden friend - preferred
    // return type deduced as std::partial_ordering
    friend auto operator<=>(point p1, point p2) = default;

private:
    double x_;
    double y_;
};

auto main() -> int {
    auto const p2 = point{1.0, 2.0};
    auto const p1 = point{1.0, 2.0};
    std::cout << "p1 == p2 " << (p1 == p2) << '\n';
    std::cout << "p1 != p2 " << (p1 != p2) << '\n';
    std::cout << "p1 < p2 " << (p1 < p2) << '\n';
    std::cout << "p1 > p2 " << (p1 > p2) << '\n';
    std::cout << "p1 <= p2 " << (p1 <= p2) << '\n';
    std::cout << "p1 >= p2 " << (p1 >= p2) << '\n';
}

Overload: Spaceship Operator

// For int-based point
auto const ordering = (p1 <=> p2) == std::strong_ordering::equal;
std::cout << "p1 <=> p2 yields equal " << ordering << '\n';
// For double-based point
auto const ordering = (p1 <=> p2) == std::partial_ordering::equivalent;
std::cout << "p1 <=> p2 yields equivalent " << ordering << '\n';

Overload: Spaceship Operator

std::partial_ordering::less
std::partial_ordering::equivalent
std::partial_ordering::greater
std::partial_ordering::unordered
std::weak_ordering::less
std::weak_ordering::equivalent
std::weak_ordering::greater
std::strong_ordering::less
std::strong_ordering::equal
std::strong_ordering::greater
  • Floating-point numbers
  • Complex numbers
  • 2D points
  • Case-insensitive strings
  • Integers
  • std::string

Example types

#include <compare>

Overload: Spaceship Operator

#include <compare>
#include <iostream>

class point {
public:
    point(int x, int y)
    : x_{x}
    , y_{y} {}

    friend auto operator==(point, point) -> bool = default;

    friend auto operator<=>(point const p1, point const p2) -> std::partial_ordering {
        auto const x_result = p1.x_ <=> p2.x_;
        auto const y_result = p1.y_ <=> p2.y_;
        return x_result == y_result ? x_result
                                    : std::partial_ordering::unordered;
    }

private:
    int x_;
    int y_;
};

lecture-3/demo355-relation2.cpp

COMP6771 20T2 - 3.2 - Operator Overloading

By cs6771

COMP6771 20T2 - 3.2 - Operator Overloading

  • 1,115