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;
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
- 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.
Unusual operators
A few operators are binary operators, even though they look odd:
alpha[beta]
is the binary operator []
with left operand alpha
and right operand beta
.
gamma(delta)
is the binary operator ()
with arguments
with left operand gamma
and right operand delta
.
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 entry 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
- Return whatever makes sense for your operation.
operator()
is const, because it doesn’t change the data.
operator[]
is not const, because it returns a read-write
reference to the data, allowing changes to the data.