Show Lecture.OperatorFunctions as a slide show.
CS253 Operator Functions
The List
- These operators can be overloaded:
- unary:
+ - *
- binary:
+ - * / % ˆ & | << >>
+= -= *= /= %= ˆ= &= |= <<= >>=
~ !
=
== != < > <= >=
&& ||
++ --
, ->* -> () []
new new[] delete delete[]
""
suffix
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:
- create new operators (sorry, no
**
, >>>
, or ≠
)
- can’t change the lexical analyzer or parser
- redefine existing bindings (can’t make
"zulu"+3
do concatenation)
- because C++ is extensible, not mutable
- change precedence or grouping of existing operators
- because C++ is extensible, not mutable.
Arguments
An operator can be a method or a plain function.
A unary operator (such as !
) has one operand:
- If it’s a free function, it has one argument, the only operand.
- If it’s a method, the operand is the current object,
and the method has no arguments.
A binary operator (such as /
) has two operands:
- If a free function, it has two arguments, the left & right operands.
- If a method, the left operand is the current object,
and the right operand is the only argument to the method.
Method or not?
Rule of thumb: Make an operator a method, unless you can’t.
- It can’t be a method if the left operand is a built-in type,
because built-in types don’t have methods.
- It can’t be a method if the left operand is not your class,
because that class is already written. You can’t add to it.
- In another ten slides or so, my opinion will change.
Arguments
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
- Yeah, I was being clever, there.
- Both
*
and +
can be either unary or binary operators.
int a, *p = &a; // a declaration, not an expression
*p = 3*4; // unary *, binary *
cout << +a + 5; // unary +, binary +
17
- The compiler works it out based on the number of arguments,
implicit + explicit.
Spell it out
- The compiler doesn’t deduce methods from other mathematically-equivalent
methods. That’s your job.
- You have to define every operator that you need:
class Fraction {
public:
Fraction operator+(double) { return *this; }
};
int main() {
Fraction a, b;
a = b + 2.3; // ok
a = 4.5 + b; // bad
}
c.cc:8: error: no match for 'operator+' in '4.5e+0 + b' (operand types are
'double' and 'Fraction')
- Defining
operator+
does not create operator+=
,
or operator++
.
- Defining preincrement doesn’t even define postincrement!
Use previous work
The Hagia Sophia
Consider these addition methods:
Fraction operator+(const Fraction &)
Fraction &operator+=(const Fraction &)
Fraction &operator++() // pre
Fraction operator++(int) // post
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)
:
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')
operator+
tries to return an int, but it’s converted to a
Number
by implicitly calling the ctor that takes an int.
b = 3+c
fails, because no method matches it.
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
“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?
- If the class doesn’t interact with other types, then just make
everything methods (remember, method = function in a class).
- If
class Foo
interacts with another type Bar
(maybe a built-in type, like int):
- Assignment-like operators (
=
, +=
, *=
, …) only have
Foo
on the left, so they should be methods of class Foo
.
- Unary operators (
!
, ~
, *
, …) can only have Foo
as
an argument, so they should be methods of class Foo
.
- Binary operators (
+
, -
, *
, /
, …) could have Foo
or Bar
on the left, so they should be non-member free functions.
- If a
Foo
ctor exists that takes a Bar
, then only write
Foo
functions that take Foo
arguments, and let the compiler
implicitly call the conversion ctor when you call them with a Bar
.
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:
- have clear mathematical analogues
- or mimic existing C++ operators (e.g.,
+=
)
- or make sense by analogy (e.g., file
+
file could
concatenate the contents of two files).
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.