Benad's Web Site

I already noted before that user-defined labels in programming languages is the simplest form of "Software Design". At the same time, the fact that only a subset of the programming language can be customized is a good thing. When reading some code, one can expect a level of formalism where the reader doesn't fear that base semantics may mean something completely different without a deep knowledge of how the language was customized.

This is where "operator overloading" makes me uneasy. The root idea is simple: A class can be made to support operator operations through function members named in special ways, making class instances be able to behave like a primitive type of a language. For example, you could define a "Fraction" class which can be used in addition, subtraction and so on like a native double.

When operator overloading try to mimic as closely as possible the original semantics of the built-in operators, then everything is fine. But quickly programmers started twisting those semantics. With strings, the "+" operator now means string concatenation. With the cout instance, you use the left bit shift operator "<<" to output strings to the console. It's not so bad through when you overload operators to introduce completely new semantics, but still those are not well self-documented until you see sample code, unlike defining functions with descriptive names.

For some reason, in the C++ language you can overload nearly every single operator, including casting, assignment (a = b), pointer resolution (*a), array index (a[0]) and even function arguments (a(1, 2)). So, of course, programmers used these to completely abuse the semantics to the point where C++ start behaving like other programming languages. For example, it is assumed that the assignment operator produces copies of the instances. Well, many programmers cleverly overloaded various operators to make certain class behave like in Java, which copies references, and add some garbage collection based on reference counting. At this point, a simple "a = b;" statement may act different than the rest of C and C++, just because some overzealous programmer wanted to introduce some garbage collection in the language.

And so, with operator overloading, you can never tell for sure what a statement does unless you know in advance if every since class used in the statement overloads any operator. Worse, in C++ a class can start acting like a pointer and an array at the same time, meaning that you can't really tell what the class is conceptually supposed to represent in term of the "base" C++ concepts.

Operator overloading in C++ makes it too easy for inexperienced programmers to produce unreadable and unmaintainable code. Unless you work with programmers that are willing to have a deep understanding of C++ before writing any code, it would be safer to simply to restrict yourself to a strict subset of the C++ language. At that point, it would be easier to use another programming language altogether.

Below is some sample C++ code of a class abusing operator overloading, to comical effect:

#include <iostream>
using namespace std;

template <class T>
class Fraction
{
public:
    Fraction(T a, T b)
    {
        this->a = a;
        this->b = b;
    }
    double toDouble()
    {
        return (double)a / (double)b;
    }
    Fraction<T> &operator=(const Fraction<T> &in)
    {
        a = in.a;
        b = in.b;
        a++; // Surprise!
        return *this;
    }
    T operator[](int pos)
    {
        if (pos == 0) {
            return a;
        }
        else if (pos == 1) {
            return b;
        }
        throw "Uh oh";
    }
    T operator()(int pos)
    {
        return (*this)[pos];
    }
    Fraction<T> *operator->()
    {
        return this;
    }
    Fraction<T> &operator*()
    {
        return *this;
    }
private:
    T a;
    T b;
};

int main(int argc, char* argv[])
{
    try {
        Fraction<int> f = Fraction<int>(3, 4);
        Fraction<int> g = Fraction<int>(1, 2);
        g = f;
        // g is still not equal to f here

        cout << f.toDouble() << " " << g.toDouble() << endl;
        cout << f[0] << " " << f[1] << endl;
        cout << f(0) << " " << f(1) << endl;
        cout << f->toDouble() << endl;
        cout << (*f).toDouble() << endl;
    }
    catch (const char *s) {
        cout << "Got exception: " << s << endl;
    }

    return 0;
}

Published on May 21, 2013 at 20:00 EDT

Older post: On C++ Templates

Newer post: On Bashing C++