Syntactic Aspartame – Recreational Operator Overloading
Sander Stoks
Operator overloading seems to be a controversial feature. There are opponents
who feel it is merely syntactic sugar with too much potential for abuse, and it
should never have made it into the language. However, one could object that
pObject->oo(x)
is merely syntactic sugar for Foo(pObject, x)
and while many people would agree that event += observer
is less clear than
event.Register(observer)
, many other people (especially programmers dealing
with scientific applications) would certainly not like to go back to a =
add(mul(p, q), r)
.
In fact, most of the people I know who learned C++ as a "second language" coming from
C, Pascal, or Fortran pointed out operator overloading as one of the most exciting
features of the language. In some cases (including my own) this lead to overzealous
overloading at first. The first thing most of us tried is defining a "power" operator
(didn't you?). It quickly turns out we couldn't get away with defining
operator**
(some other languages have the **
operator for
"raised to the power of") because this is parsed differently from what we expected.
Next thing to try is operator^
, which works but leads to unexpected results
because of operator precedence rules.
So, maybe the initial enthousiasm has cooled off a little bit, but operator overloading is still very nice. Especially the fact that a matrix-times-vector multiplication looks much like you would write it down on paper is appealing to programmers dealing with numerical algorithms. Yet, they inevitably run into a problem with vectors.
Vectors have two kinds of multiplication, known as the dot (or inner) product, and the cross (or outer) product. For three-dimensional vectors, these are defined as
a . b = ax*bx + ay*by + az*bz
a × b = (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx)
Unfortunately, we don't have "centered dot" or "cross" symbols at our disposal, and
C++ doesn't let you define new infix operators. You're stuck with only one
operator*
for vectors, and you'll have to pick either the dot product or
the cross product for your implementation.
To a mathematician, the operator symbol used for the vector product is actually often redundant. It is clear from the context which one of the two is meant: if the result appears in a scalar expression, it was the dot product; if it is in a vector expression, it was the cross product. This observation lands us on the slippery slope of Return Type Overloading, which is even more controversial than operator overloading, and which C++ doesn't support anyway.
Or does it?
As the basis of my examples, I will use a simple Vector3
type, along
with inner()
and outer()
functions for both types of products:
struct Vector3 { Vector3(double x_, double y_, double z_) : x(x_), y(y_), z(z_) {} double x; double y; double z; }; double inner(const Vector3& lhs, const Vector3& rhs) { return a.x*b.x + a.y*b.y + a.z*b.z; } Vector3 outer(const Vector3& lhs, const Vector3& amp;rhs) { return Vector3(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x); }
Since I will focus on multiplication, I have elided all the other stuff. If you could have code like
Vector3 a(1, 2, 3); Vector3 b(7, 5, 6); double dp1 = a*b; Vector3 cp1 = a*b;
it would be apparent that the former multiplication is meant to be a dot product, and the latter a cross product. The first thought would be to simply define
Vector3 operator* (const Vector3& lhs, const Vector3& rhs) { return outer(lhs, rhs); } double operator* (const Vector3& lhs, const Vector3& rhs) { return inner(lhs, rhs); }
but this is Return Type Overloading, and you can't. What you can do, however, is
use Copliens proxy class pattern. You do not return a Vector3
or a
double
from your multiplication immediately, but a special "intermediate
result":
VectorProduct operator*(const Vector3& lhs, const Vector3& rhs) { return VectorProduct(lhs, rhs); }
You define this VectorProduct
class to be convertible into a
Vector3
or a double
, depending on how it is "further
processed":
class VectorProduct { friend VectorProduct operator*(const Vector3& lhs, const Vector3& rhs); VectorProduct(const Vector3& lhs, const Vector3& rhs): lhs_(lhs), rhs_(rhs) {} const Vector3& lhs_; const Vector3& rhs_; public: operator double() const { return inner(lhs_, rhs_); } operator Vector3() const { return outer(lhs_, rhs_); } };
Note that its constructor is private and only accessible from the
operator*
, because that's the only context it makes sense in. With only
these two additions, we have a poor man's RTO. Of course, one of the main problems
with RTO is the ambiguity when an expression is used where more than one of its
resulting types is valid. For example, cout << a*b
will make your
compiler complain about ambiguity, and will have to be specified with either
cout << double(a*b)
or cout << Vector3(a*b)
.
It turns out there is also a way to define new infix operators. So as not to spoil the surprise, I will fist show the resulting client code:
Vector3 a(1, 2, 3); Vector3 b(7, 5, 6); double dp2 = a dot b; Vector3 cp2 = a cross b;
This looks rather un-C++. It uses Copliens trick twice, with a bit of preprocessor
added. When shown the definitions of dot
and cross
, you can
probably guess how it works:
#define dot *Dot* #define cross *Cross*
(Of course, the convention is to write preprocessor definitions in BIG_UGLY_CAPS,
but that would have "given it away" instantly.) Dot
and Cross
are defined as follows:
const struct Inner {} Dot; const struct Outer {} Cross;
The idea is that a dot b
is parsed into (a*Dot)*b
, where
you can make up your own definition of a*Dot
; in this case, resulting in
an intermediate result - sort of a "half-way dot product". I've thrown in some
gratuitous template code because this makes it reusable for the cross product
later on:
template <typename T> struct VecOp { VecOp(const Vector3& v) : v_(v) {} const Vector3& v_; }; VecOp<Inner> operator* (const Vector3& lhs, const Inner& op) { return VecOp<Inner>(lhs); }
This intermediate result is then multiplied by b, and this multiplication is again overridden to yield the expected dot product:
double operator* (const VecOp<Inner>& lhs, const Vector3& rhs) { return inner(lhs.v_, rhs); }
The version for the cross product is very similar:
Vector3 operator* (const VecOp<Outer>& lhs, const Vector3& rhs) { return outer(lhs.v_, rhs); }
You may be tempted to use a similar trick to give you an infix ** operator, but
this has some serious drawbacks. First, observe that dereferencing a
double
has no meaning. This is exactly what the compiler tells you if
you accidentally type
double a = 3; double b = 2; double c = a ** b;
My compiler says "Illegal indirection", because a ** b
is parsed as
a*(*b)
. If you could somehow make *b
return a proxy type,
you would be ready. While you are free to overload the "unary *" operator for your
own types, you cannot overload operator*(double)
because it's a builtin
type (even though this particular operator isn't provided). You therefore need one
change in the spelling to make the above code work:
double a = 3; Double b = 2; double c = a ** b;
Note that b
is now of type Double
, a little wrapper around
the builtin double
:
struct Double { Double() {} Double(double d): d_(d) {} operator double() { return d_; } double d_; };
so that you can define
class Exponent { friend Exponent operator*(const Double& d); Exponent(const Double& d): d_(d.d_) {} public: const double& d_; }; Exponent operator*(const Double& d) { return Exponent(d); } double operator*(double lhs, const Exponent& rhs) { return pow(lhs, rhs.d_); }
Unfortunately, this can never work for literals. This is a nuisance since this is what a "power" operator would be used most often with. With a little help from the preprocessor:
#define POW **(Double)
you could type
double a = 3; double c = a POW 2;
But note that this operator still gets its precedence wrong, since you would
expect that 2*a**b
is parsed as 2*(a**b)
, yet it is instead
parsed as (2*a)*(*b)
, or (2*a)
to-the-power-of b
.
Therefore, it is best to just stick to using the regular pow(a, b)
function. Note that for the vector product examples above precedence was not a problem
because of the linearity of the products.
Next, I will show one last trick which makes range checks look more "mathematic". Beginning C programmers often type in something like this:
if (lower < x < upper) { ... }
instead of
if (lower < x && x < upper) { ... }
Unfortunately, the VC6 and VC7.1 compilers only emit a warning if you try the
former syntax: "unsafe use of type 'bool
' in operation". Even worse:
Many compilers (I tried GCC 2.95 and GCC 3.3.3, and even Comeau 4.3.3 with the
-remarks
option) don't feel there's anything wrong with it at all, yet
the meaning is quite opposite from what you would expect. It may or may not evaluate
to the correct answer: If lower < x
, the first part of the expression
is true
, and if upper > 1
, the entire expression evaluates
to true
too (which may be correct). However, if lower >= x
,
then the first part is false
; the expression becomes if (0 <
upper)
, and if upper
is then positive, the entire expression also
evaluates to true
(which is definitely incorrect)!
However, if you are prepared to make at least lower be of a custom type
(Double
, for instance), we can apply the proxy trick once more:
class Comparison { friend Comparison operator<(const Double& lhs, const double& rhs); Comparison(const double& lhs, const double& rhs) : lhs_(lhs), rhs_(rhs) {} public: const double& lhs_; const double& rhs_; operator bool() const { return lhs_ < rhs_; } }; Comparison operator< (const Double& lhs, const double& rhs) { return Comparison(lhs.d_, rhs); } bool operator< (const Comparison& lhs, const double& rhs) { return lhs && lhs.rhs_ < rhs; }
This makes the former if
statement work. Of course, this is highly
dangerous code, especially if your compiler doesn't emit a warning if you accidentally
leave the lower bound a builtin type.
The observation that comparison operators need not return a bool
is
quite interesting though, and I will conclude with a way to define new infix operators
using a syntax which looks slightly more C++-ish by maximizing the use of angle brackets.
First, let me set the stage. Given a simple type representing rectangles:
typedef struct { int left; int top; int right; int bottom; } Rect;
you want to write a function which tells you whether a certain rectangle is fully contained within another. One way of doing this would be a simple function:
bool contains(const Rect& lhs, const Rect& rhs) { return lhs.left <= rhs.left && lhs.top <= rhs.top && lhs.right >= rhs.right && lhs.bottom >= rhs.bottom; }
but when you see the function being used, you can't immediately guess which rectangle
is supposed to be contained within which: if (contains(a, b))
is at least
slightly ambiguous (to the human reader, that is). If you make it a member function of
the struct
, the ambiguity goes away: if (a.contains(b))
but we've been taught not to add too many member functions (or member operators, for that
matter) if these functions can be written as external functions just as well. It looks
like this "containment" function would be an ideal candidate for a new infix operator.
Let's re-use the trick this article started out with using a slightly different syntax:
const struct contains_ {} contains; template <typename T> struct ContainsProxy { ContainsProxy(const T& t): t_(t) {} const T& t_; }; template <typename T> ContainsProxy<T> operator<(const T& lhs, const contains_& rhs) { return ContainsProxy<T>(lhs); } bool operator>(const ContainsProxy<Rect>& lhs, const Rect& rhs) { return lhs.t_.left <= rhs.left && lhs.t_.top <= rhs.top && lhs.t_.right >= rhs.right && lhs.t_.bottom >= rhs.bottom; }
With this bit of code, you can write
if (a <contains> b) ...
which may be surprising to a human reader, but not ambiguous. Also,
given a simple Point
type with x
and y
members,
it is easily extended:
bool operator>(const ContainsProxy<Rect>& lhs, const Point& rhs) { return rhs.x >= lhs.t_.left && rhs.x <= lhs.t_.right && rhs.y >= lhs.t_.top && rhs.y <= lhs.t_.bottom; }
Finally, let's reveal why ContainsProxy
was made a template. When you
define
template <typename U, typename V> bool operator>(const ContainsProxy<V>& lhs, const U& rhs) { return find(lhs.t_.begin(), lhs.t_.end(), rhs) != lhs.t_.end(); }
you can even do
vector<int> v; if (v <contains> 42) ...
Of course, the opponents of operator overloading will have turned away in disgust by now, but the fact that you shouldn't do something, doesn't mean you can't.
By the way: my introductionary computer programming book is available here. It uses C and stays away from the type of exotic Spielerei described in this article.