Thursday, July 28, 2011

Operator overloading, OO the C++ way

If you question around about a mainstream object oriented language, most people will point to Java, or C#. Sure that C++ has classes, and objects, inheritance, polymorphism... but it's not really object oriented, there are still non-member functions, and that is just so no-OO. Or is it?

There are different programming paradigms, some of them better suited for some problems than others. You can pretty much solve all problems with any paradigm, but some paradigms help modeling the domain of the problem easier than others. What is important to remember is that programming is not about the tools you use, programming is about solving a problem.

In pure OO everything is an object, and operations are executed on one object and each object has a set of methods that form its interface. In C++ not everything is an object, and the interface of a type is not just the set of methods, but it also includes the set of free functions that are defined in the same namespace

From a design standpoint the question is how does the operations in the domain map to the language? Does every operation belong to a particular type? Or is there space for free functions?

Study case

Designing a numeric type (be it a biginteger, a decimal or just a simple integer type with a range larger than that those available in your platform) is a good exercise. The domain is well understood and we can focus on the design of the interface. We don't even need to think of names for the operations, overloadable operators fit the domain perfectly. The C++ language allows for overloading of most operators both as a member function and a free function, so it will not force a decision upon us.

Creating a number

For starters, we want to be able to construct our number and we want to allow conversions from other arithmetic types, for simplicity just consider double. The compiler can convert any integral or floating point number into double for us, and this will enable creation of number from any other arithmetic type.

Because the first constructor can be used to perform implicit conversions from double there is no need to provide an assignment operator that takes a double, it will work out of the box. First the right hand side will be converted, and then the assignment operator will be executed.

Now we can add some arithmetic operations, using addition as a pattern. In the domain (math) addition takes two elements and produces a third unrelated element that is the result of the operation. In programming it is common for efficiency reasons to provide a += operator that behaves like addition but stores the result in the first element. Then we can define regular addition in terms of the previous +=: adding two numbers together can be expressed as making a copy of one of them and then applying += to that copy with the second value.

Interface of operator+= and operator+

The += operator is a natural candidate for a member function. The operation is applied to a particular instance, it is a feature of that instance. Then we have operator+. As described above, addition takes two elements and yields a third element with the aggregate value. It is not more of an operation on the first argument than it is on the second, so there is no compelling reason to make it a member function, and we should prefer a free function as that treats both arguments similarly.

The signature of operator+ follows a common idiom when overloading operators, but for now just focus on the fact that it is a free function.

Differences between a member function and a free function when overloading

The main difference is how overload resolution is performed. When the operator is implemented as a free function, both arguments have equal standing with respect to the compiler, any conversion that can be applied to the right hand side can also be applied to the left hand side.

In the member function case, the two arguments don't have equal standing, the left hand side argument must be of the type that implements the operator. The compiler is able to perform implicit conversions on the right hand side, but the left hand side is sacred.

The same decisions we made in the design are translated into the code: when we implemented operator+ as a free function claiming that the operation is no more of the first argument than the second, we got symmetry from the compiler. When we decided that operator+= was a member function we got asymmetry, it can only be called on objects of type number.

You can argue whether C++ is better or worse at object orientation than other languages, but there are certain things that are harder to model while tied to the constraints of everything is an object and all operations are methods.

I have kind of hand waved over quite a few things of the design. One of the things left aside are the Rule of the Three (if the implicitly generated copy constructor and assignment were not good enough, we probably need a destructor, but not having provided an implementation I just ignored it). The signature of the operators are interesting by themselves (can you think of anything I should have done differently? Assume that this is a big number implementation that has to manage some expensive resource. Drop me an email at definedbehavior at gmail dot com), as is the effect of friendship on this problem, note that we do not need it, we can implement operator+ on top of the existing member operator+=, but maybe it could help us somehow? But that's a story for another day.

No comments:

Post a Comment