Having looked at how to create a class representing a three-dimensional vector in the previous article, we’re going to look at how to add more operator-related functionality to it. We’ll start off with multiplying and dividing by a scalar, and move on to providing non-member operator overloads.
It is important to understand that the parameter(s) to operator overload functions can be of any type. We make use of this facility when multiplying by a scalar, as shown in the following two functions defined within the class body:
class Vec3d final { // ... Vec3d& operator*=(const T& rhs) { data[0] *= rhs; data[1] *= rhs; data[2] *= rhs; return *this; } Vec3d& operator/=(const T& rhs) { return operator*=(T{1} / rhs); }
In the above code, operator*=
takes a single parameter of type const-reference-to-T
, where T
is the element type for the class. An alternative generic form, which would allow for multiplying by other types such as int
could be:
template<typename U> Vec3d& operator*=(const U& rhs) { // ...
The only risk to such generality is the possibility of long compilation error messages related to invalid template instantiations.
Notice that operator/=
has been defined in terms of operator*=
by passing the reciprocal of rhs
as the parameter. Operator overloading member functions are just normal member functions with funny names, and can be invoked as such.
Now let’s look at providing some more user-friendly syntax to our types, such as allowing v + w
or v * 2.0
as valid expressions. This involves writing free-function (global) operators with the parameters matching the intended types involved. Here is operator+
defined in terms of (member) operator+=
:
template<typename T> inline Vec3d<T> operator+(const Vec3d<T>& lhs, const Vec3d<T>& rhs) { return Vec3d<T>(lhs.x() + rhs.x(), lhs.y() + rhs.y(), lhs.z() + rhs.z()); }
In this one-line function, the FP-style of creating a new object and returning it by value (assuming use of RVO) should be seen to be efficient as there should be no overhead from using the public member functions x()
, y()
and z()
. The parameters are by convention called lhs
and rhs
(for left-hand-side and right-hand-side of the operator +
) and this function has been declared inline
so that it can be defined in the same header file as class Vec3d
.
(An alternative standard boilerplate for writing the addition operator would be to initialize a new variable result
from lhs
and then utilize member operator+=
with rhs
as its parameter, before returning variable result
, however for our class the above probably has slightly superior performance and readability.)
Care should be taken when mixing member and global operator
overloads that there is no duplication, as this would result in an ambiguous function call (in exactly the same way as for conventional functions). In other words, you should not define a member operator@
as well as a global operator@
with the same signatures (taking into account the this
parameter as implied equivalent to the first parameter for a global operator function).
The subtraction operator can be defined in exactly the same way:
template<typename T> inline Vec3d<T> operator-(const Vec3d<T>& lhs, const Vec3d<T>& rhs) { auto result = lhs; result -= rhs; return result; }
For multiplication by a scalar we will need two functions because we want to allow 2.0 * v
as well as v * 2.0
. Here they are in full:
template<typename T> inline Vec3d<T> operator*(const Vec3d<T>& lhs, const T& rhs) { return Vec3d<T>(lhs.x() * rhs, lhs.y() * rhs, lhs.z() * rhs); } template<typename T> inline Vec3d<T> operator*(const T& lhs, const Vec3d<T>& rhs) { return operator*(rhs, lhs); }
Notice the second operator*
is defined by calling the first as a normal function (with the order of the parameters switched). As before, we could have used template<typename T, typename U>
and const Vec3d& lhs, const U& rhs
to allow other scalar types, but only if operator*=
supported them too.
We only need one operator/
which could be defined in terms of either operator/=
(as shown), or operator*
:
template<typename T> inline Vec3d<T> operator/(const Vec3d<T>& lhs, const T& rhs) { return Vec3d<T>(lhs.x() / rhs, lhs.y() / rhs, lhs.z() / rhs); }
With the following main program:
int main() { Vec3d v(2.0, 4.0, 3.0), w(1.0, 5.0, -2.0), z = v + w; std::cout << z << '\n'; std::cout << 3.0 * v << ' ' << w / 2.0 << '\n'; }
The following output is obtained:
(3,9,1) (6,12,9) (0.5,2.5,-1)
That just about wraps things up for this article. We have shown operator*=
with a parameter of different type to *this
as well as global operators defined as needed. In the next article we’ll discuss “exotic” operators such as ->
, as well as ones which shouldn’t be redefined, such as &&
.