C# - Operator Overloading Part 2 [Intermediate]


Welcome back to the world of psychotic things you should never do if you want readable code! And in this particular case, I'll be talking about what happens in C# when you mix operator overloading with inheritance and polymorphism. If you need a refresher on C# operator overloading, you should check out part 1 of this tutorial, at Overloading Part1. Now, without further ado, lets jump right in.

To start this off, first we are going to need a bunch of code to test the different cases that occur because of polymorphism. Below are three (rather empty) classes with some overloaded operators in them:

public class BaseClass
{
  public static BaseClass operator +(BaseClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Addition in: BaseClass");
    return new BaseClass();
  }

  public static BaseClass operator /(BaseClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Division in: BaseClass");
    return new BaseClass();
  }
}

public class ExtendedClassA : BaseClass
{
  public static ExtendedClassA operator +(ExtendedClassA arg1, ExtendedClassA arg2)
  {
    Console.WriteLine("Addition in: ExtendedClassA");
    return new ExtendedClassA();
  }

  public static ExtendedClassA operator -(ExtendedClassA arg1, BaseClass arg2)
  {
    Console.WriteLine("Subtraction in: ExtendedClassA");
    return new ExtendedClassA();
  }

  public static ExtendedClassA operator *(ExtendedClassA arg1, ExtendedClassA arg2)
  {
    Console.WriteLine("Multiplication in: ExtendedClassA");
    return new ExtendedClassA();
  }
}

public class ExtendedClassB : BaseClass
{
  public static ExtendedClassB operator +(ExtendedClassB arg1, ExtendedClassB arg2)
  {
    Console.WriteLine("Addition in: ExtendedClassB");
    return new ExtendedClassB();
  }
}
 
So here we have three classes: BaseClass, ExtendedClassA, and ExtendedClassB. As you probably guessed from the names, ExtendedClassA and ExtendedClassB extend the class BaseClass. Now, you might notice that these operator overloads are a little odd. But thats ok, because for this testing, all we really care about is which operator function gets called - and writing to the console is an easy way to see that.

Here is the first peice code we will be using to test these operator overloads:
public void TestOverloads()
{
  BaseClass baseClass = new BaseClass();
  ExtendedClassA extA = new ExtendedClassA();
  ExtendedClassB extB = new ExtendedClassB();
  BaseClass extAinBase = new ExtendedClassA();
  BaseClass resultholder;

  resultholder = baseClass + baseClass;
  resultholder = extA + extA;
  resultholder = baseClass + extA;
  resultholder = extA - extA;
  resultholder = extA / extA;
  resultholder = extA + extB;
  resultholder = extA + extAinBase;
  resultholder = extAinBase + extAinBase;
}
 
The first couple lines are just setup, where we make a couple instances of the test classes to use. What we really care about is the output of the resultHolder = lines, which looks like:
Addition in: BaseClass
Addition in: ExtendedClassA
Addition in: BaseClass
Subtraction in: ExtendedClassA
Division in: BaseClass
Addition in: BaseClass
Addition in: BaseClass
Addition in: BaseClass
 
So lets go through these lines one by one and see what they mean. First, we have the line resultholder = baseClass + baseClass producing the result Addition in: BaseClass. This makes perfect sense - adding two instances of BaseClass will call the addition overload defined in BaseClass. The next line, resultholder = extA + extA producing Addition in: ExtendedClassA also makes sense, it shows that adding two instances of ExtendedClassA will call the addition overload defined in ExtendedClassA.

The third line is where it starts to get interesting, where we have resultholder = baseClass + extA printing out the line Addition in: BaseClass. There is no addition overload in either ExtendedClassA or BaseClass that has the signature (BaseClass arg1, ExtendedClassA arg2), so your first guess might be that it should fail. But instead, it treats the ExtendedClassA instance like a BaseClass instance, calling the addition overload in BaseClass.

The fourth line - resultholder = extA - extA printing Subtraction in: ExtendedClassA - shows that even though ExtendedClassA does not have an overload that matches the signature exactly, it will try down casting the arguments in order to find a signature that matches. And in this case it did, it called the subtract overload in ExtendedClassA, which takes an instance of ExtendedClassA and an instance of BaseClass.

A side note on the fourth line - because of what C# does here, there is sometimes potential ambiguity. For instance, take a look at the following code:
public class MessedUpClass : BaseClass
{
  public static MessedUpClass operator -(MessedUpClass arg1, BaseClass arg2)
  {
    Console.WriteLine("Subtraction in: MessedUpClass. BaseClass arg2.");
    return new MessedUpClass ();
  }

  public static MessedUpClass operator -(BaseClass arg1, MessedUpClass arg2)
  {
    Console.WriteLine("Subtraction in: MessedUpClass. BaseClass arg1.");
    return new MessedUpClass ();
  }
}
 
If you try actually use that subtraction overload with two instances of MessedUpClass, you will get a compiler error:
MessedUpClass a = new MessedUpClass();
a = a + a;
//Error: The call is ambiguous between the following methods or properties: 'MessedUpClass.operator -(MessedUpClass, BaseClass)' and 'MessedUpClass.operator -(BaseClass, MessedUpClass)'

Which makes sense - how is the compiler supposed to know which object it should downcast in order to make the call fit one of those signatures?

OK, now back to the output of our test function. On line five, we have resultholder = extA / extA giving us the result Division in: BaseClass. This is an extension of line 4 - here there was no overload in ExtendedClassA that matched, but it down casted both arguments and used the division overload in BaseClass.

On line six, we add an instance of ExtendedClassA with an instance of ExtendedClassB: resultholder = extA + extB which gives us the line Addition in: BaseClass. Here, we have another example of down casting. There is no addition overload in either ExtendedClass that will take one argument as ExtendedClassA and one as ExtendedClassB. But the compiler is able to downcast both and use the addition overload in BaseClass. Again, like with variations of line four, if you go crazy with overloads in this type of case, the compiler is likely to throw ambiguity errors.

Next is line seven: resultholder = extA + extAinBase with the result Addition in: BaseClass. Here we have an instance of ExtendedClassA being held in a BaseClass variable. And it is here we learn that operator overloading is not actually polymorphic. If it was polymorphic, the addition overload in ExtendedClassA would have been called, because at runtime the virtual machine would have seen that extAinBase is actually an instance of ExtendedClassA. But because operator overloads are static (i.e., decided at compile time), and the compiler does not know that extAinBase is anything other than an instance of BaseClass, it has no choice other than using the addition overload defined in BaseClass.

In some ways, you should probably be thankful that operator overloading is not a dynamic (i.e., runtime) operation. In many ways, operator overload is confusing enough already - making it dynamic would make code that used it even less readable than standard overloading.

Last we have line eight adding extAinBase with itself (resultholder = extAinBase + extAinBase) and as expected using the overload defined in BaseClass (Addition in: BaseClass). Nothing surprising about that here, it follows from what was just stated about line seven.

And as yet another reiteration of the fact that overloading is not dynamic, here is a line of code that generates an error:
resultholder = extAinBase * extAinBase;
//Error: Operator '*' cannot be applied to operands of type 'BaseClass' and 'BaseClass'

There is an overload for multiplication in ExtendedClassA, but as you can see here, it ignores that completely, because again, all the compiler knows is that we are trying to multiply two instances of BaseClass (which has no multiplication overload).

And that about covers how operator overloading works with polymorphism and inheritance in C#. One side note - you can't declare operator overloads in an interface (again, because the overloads are decided at compile time, and if its just pointing at an interface, what actual function is the compiler supposed to map the overload to?).

Hope you found all this craziness interesting!

Comments