CS253: Software Development with C++

Spring 2023

Operator Functions

Show Lecture.OperatorFunctions as a slide show.

CS253 Operator Functions

The List

Yes, = is an operator, just like + is. Sure, = usually alters its left operand, whereas + doesn’t, but C++ doesn’t care about that.

What can’t you do?

You cannot:

Arguments

An operator can be a method or a plain function.

A unary operator (such as !) has one operand:

A binary operator (such as /) has two operands:

Method or not?

Rule of thumb: Make an operator a method, unless you can’t.

Arguments

Foo f;
result = *f;

There are two ways to implement the unary * operator:

free function, one argument:
returntype operator*(const Foo &);
Foo method, no arguments:
returntype Foo::operator*() const;

const is typical, but not always—it depends on the semantics of the particular operation you’re implementing.

The method has no argument because it has an implicit argument, namely, the current object, the operand.

Arguments

Foo f;
Bar b;
result = f + b;

There are two ways to implement the binary + operator:

free function, two arguments:
returntype operator+(const Foo &, const Bar &);
Foo method, one argument:
returntype Foo::operator+(const Bar &) const;

The method is const, since + doesn’t alter its left operand, unless you have wacky semantics.

The method has one fewer argument because it has an implicit argument, namely, the current object, as the left-hand operand.

Clever

int a, *p = &a;  // a declaration, not an expression
*p = 3*4;        // unary *, binary *
cout << +a + 5;  // unary +, binary +
17

Spell it out

Use previous work


The Hagia Sophia

Consider these addition methods:

There’s a lot of work in adding fractions: check for division by zero, common denominator, add, and reduce to lowest terms. Don’t do that in four different places!

The smart programmer will keep it DRY by having operator+= do the real work, have operator+ and preincrement invoke +=, and have postincrement call preincrement.

More re-use

For that matter, define operator-= in terms of operator+= (if negation is cheap for your class). Then, as before, you can have operator- and predecrement call operator-=, and have postdecrement call predecrement.

Students are often tempted to have operator+ do the real work, and have operator+= call operator+. Don’t do that. It usually works better having operator+= do the real work.

Methods

When possible, implement operator overloading as methods (as part of the class). When the method is called, the left operand is *this and the right operand is the argument. This will call a.operator+=(b):

Fraction a, b;
a += b;

This will call a.operator=(b.operator-(c)):

Fraction a, b, c
a = b - c;

The Problem with Methods

class Number {
  public:
    Number(int v) : value(v) { }
    Number operator+(Number rhs) { return value+rhs.value; }
    int value;
};
ostream& operator<<(ostream &os, Number n) { return os << n.value; }

int main() {
    Number a(1), b(2), c = a+b;
    cout << a+b << '\n';
    cout << 3+c << '\n';  // 🦡
}
c.cc:12: error: no match for ‘operator+’ in ‘3 + c’ (operand types are 
   ‘int’ and ‘Number’)

Solution #1

We could write functions for each combination of arguments, but that’s not DRY:

class Number {
  public:
    Number(int v) : value(v) { }
    Number operator+(Number rhs) { return value+rhs.value; } // Number+Number
    Number operator+(int n) { return value+n; }              // Number+int
    int value;
};
Number operator+(int n, Number rhs) { return n+rhs.value; }  // int+Number
ostream& operator<<(ostream &os, Number n) { return os << n.value; }

int main() {
    Number a(1), b(2), c = a+b;
    cout << a+b << '\n';
    cout << 4+c << '\n';
}
3
7

Solution #2

Instead, just write one non-method free function that takes two Number objects, and let int arguments get implicitly converted to Number objects:

class Number {
  public:
    Number(int v) : value(v) { }
    int value;
};
Number operator+(Number lhs, Number rhs) { return lhs.value+rhs.value; }
ostream& operator<<(ostream &os, Number n) { return os << n.value; }

int main() {
    Number a(1), b(2), c = a+b;
    cout << a+b << '\n';
    cout << 4+c << '\n';
}
3
7

Generally, keep all the parts of a class inside the class, but it’s better to do this and keep it DRY.

Efficiency

made at imgflip.com

“What about speed? All those constructor calls!

When operator+ is compiled, the compiler has already seen the trivial constructor code, and will happily inline the constructor code (i.e., copy one int) right into operator+, resulting in no ctor calls.

“Don’t copy those Number objects—pass by reference!”

Copy a 32-bit int or a 64-bit address—about the same. However, calling by reference forces the Number to be in memory, so its address can be taken, which stops the compiler from keeping a Number in a register. Slow! The rule isn’t really “pass scalars by value, objects by reference”, it’s “pass small things by value, big things by reference”.

Best Practices

When should operator functions be methods?

Abuse

Java doesn’t allow operator overloading, supposedly because it’s too confusing. There is something to be said for that.

Resist the urge to redefine every 💣 ☠️†※! operator possible. Only define those operators that:

Example of abuse

A misguided programmer might define a-b, where a and b are strings, to mean “return a, without the chars that are also in b”.

string operator-(string a, const string &b) {
    for (size_t p=0; (p = a.find_first_of(b, p)) != a.npos;)
        a.erase(p, 1);
    return a;
}

int main() {
    const string name = "Bjarne Stroustrup";
    cout << name-"aeiou" << '\n';
}
Bjrn Strstrp

Unusual operators

A few operators are binary operators, even though they look odd:

We traditionally call the first one the “subscript operator”, and the second one the “function call operator”, but that’s just convention. You can have them do whatever you like. You could define () for a std::string to be like [] but get the n th char from the end.

Example of [] and ()

class Pi {
    int digits[21] = {3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3,2,3,8,4,6};
  public:
    int& operator[](size_t n) { return digits[size(digits)-n]; }
    double operator()(size_t n) const { return sqrt(digits[n]); }
};

Pi p;
cout << p[3] << ' ' << p(4) << '\n';
8 2.23607