C++ Operator Functions, also known as operator overloading are a kind of syntactic sugar that allow developers to use natural operators (+, -, /, * etc.) on types that are not built-in, scalar types. Instead of using the compiler's implementation for these operations, developers may specify an operator function that acts on one or two custom types. For example, if a developer implements their own class ComplexNumber, addition, subtraction and other mathematical operators are a natural fit for this type, despite not being defined in the compiler. So, how does this actually look in code? Compare the following two functions.
ComplexNumber add(const ComplexNumber &n1, const ComplexNumber &n2);
ComplexNumber div_scalar(const ComplexNumber &n1, const float n2);
ComplexNumber average_with_operators(const ComplexNumber &n1, const ComplexNumber &n2) {
return (n1 + n2) / 2;
}
ComplexNumber average_with_functions(const ComplexNumber &n1, const ComplexNumber &n2) {
return div_scalar(add(n1, n2), 2);
}
Which one of these two looks cleaner? Using operator overloading can make your code significantly easier to read, if you use it the right way. Some libraries make a mockery of the operations they override; for example, STL's streams use the bit-shift-left operator (<<) for I/O operations on a stream.
Aside from the arithmetic operators touched on earlier, there are a large number of operators that may be overloaded in C++; even the de-reference and comma operators may be overloaded by a class, although it is definitely not recommended to overload operators with unexpected behaviour.
There are two ways that operator functions may be declared; firstly, as a stand-alone function that takes one or two arguments, or secondly, as a class method. If an operator function is defined as a class method, the instance that it is called on is to be treated as the (implicit) first parameter. Let's take a look at an example.
class ComplexNumber {
private:
float re, im;
public:
ComplexNumber operator+(const ComplexNumber &n) const {
ComplexNumber sum;
sum.re = this->re + n.re;
sum.im = this->im + n.im;
return sum;
}
ComplexNumber operator-() const {
ComplexNumber negated(*this);
negated.re *= -1;
negated.im *= -1;
}
friend ComplexNumber operator-(const ComplexNumber &n1, const ComplexNumber &n2);
};
ComplexNumber operator-(const ComplexNumber &n1, const ComplexNumber &n2) {
ComplexNumber diff;
diff.re = n1.re - n2.re;
diff.im = n1.im - n2.im;
return diff;
}
Here we have three operator functions defined, addition, negation
and subtraction. Addition is a binary operator - that is, it takes
two operands. operator+
has been defined as a method
of ComplexNumber, which means that 'this' is an implicit parameter
to the method - the function should calculate 'this + n'. If dealing
with types where the order is significant, say, with matrices, remember
that 'this' is always the first argument, not the second.
Secondly, we have the negation operator defined as
operator-()
. Note that it does not take any explicit
parameters; as a member of ComplexNumber, there is an implicit 'this'
parameter on which it will operate. The negation operator also shares
its name with the subtraction operator; the only difference between
the two type signatures is the presence of the second operand for
subtraction.
Finally, we have the subtraction operator. Unlike the addition and
negation examples, it is defined outside of ComplexNumber, as a
stand-alone operator function. It takes two explicit parameters,
and returns a ComplexNumber
. Note that the operator
function is accessing private members of ComplexNumber,
despite not being a member function of ComplexNumber.
As you might be aware from reading up on the
use of friend
in C++, one way to grant access to
private and protected members of a class is to bless a function
with the friend
keyword, which we have done.
Operator overloading isn't just for custom number-related classes. C++ strings, data structures and streams all make extensive use of various operators to remove the noise from C++ coding. Let's examine a few examples.
C++'s std::string type implements a number of operators for convenience,
including operator+
and operator+=
for
concatenation; operator[]
for accessing characters inside
the string, and operator==
to test equality of the underlying
value.
void string_comparison() {
std::string a = "Hallo World";
std::string b = "Hello";
a[1] = 'e';
b += " World";
if (a == b) {
std::cout << b << std::endl;
}
}
If std::string did not implement operator==
, comparisons
would be done by way of pointers instead of the underlying value. Providing
operator overloads leads to a more intuitive outcome, and cleaner code.
C++'s Standard Template Library,
or STL, offers a number of standard templated data structures that you
can use, including vector
, set
, and
deque
. These data structures use operator overloading to make
data lookup a breeze, and are fairly well optimised for the common case.
void display_vector() {
std::vector<int> v;
v.push_back(42);
std::vector<int>::iterator it = v.begin();
int a[1] = {42};
std::cout << a[0] << " == " << v[0] << " == " << *it << std::endl;
}
A vector implements the subscript operator, operator[]
so
that it can be treated like a simple array despite the underlying
storage looking nothing like a simple, linear array. Likewise, STL
iterators override the dereference operator, operator*
to
provide access to the current item that the iterator is pointing to.
STL iterators also override the increment operator, operator++
to move between elements in the underlying container.
Somewhat unintuitively, C++ streams overload operator<<
to push data into output streams, and operator>>
to pull
data from input streams. Interestingly enough, C++ streams also have the
concept of manipulators that can be passed to a stream with the
<<
and >>
operators to provide formatting
control such as setf
and hex
.
void stream_ops() {
uint32_t value = 0xCAFE47;
std::cout << std::setw(8) << std::hex() << std::setfill('0') << value;
}