tucuxi.org

C++ Operator Functions

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.

Declaring operator functions

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.

Not Just Numbers

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.

Strings

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.

Iterators

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.

Streams

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;
}