CS253: Software Development with C++

Fall 2021

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

Either could be const, depending on the semantics.

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: 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

Maybe, maybe not. 🤷 Call it remove_chars(), instead.