C# - Operator Overloading Part 1 [Intermediate]


Operator overloading in C# is pretty easy and extensible, and there is plenty of documentation out there on the internet for most kinds of operator overloading. In fact the MSDN docs themselves do a good job of explaining how to do basic operator overloading. But what is not covered anywhere are the fringe cases of operator overloading - how overloading interacts with inheritance, and other things even weirder than that.

Now, you might be asking why anyone would ever want to do anything crazy like that - and the answer is probably never. Operator overloading by itself in code is often questionable - there are many cases where it actually decreases the readability of the code. So crazy operator overloading, where what actually happens behind the scenes is even more confusing than normal, is probably something to avoid at all costs. But hey, we aren't here to debate techniques - we're here to do some crazy overloading!

Basic Operator Overloading
Just in case you need a quick overview of basic operator overloading in C#, here you go. To overload a unary operator for a class, you write a method in that class with the following signature:
public static SomeClass operator ++(SomeClass arg)

For instance, this signature is for overloading the ++ operator for SomeClass. The declaration is actually pretty simple - most of the stuff here is normal for a method declaration. The only odd things are the 'operator' keyword (which lets .NET know that this a operator overload function), and the name of the function, instead of being an actual name, is the operator being overloaded. The public and static keywords make sense (in fact it won't compile without them), because it wouldn't make much sense to have a private or non-static operator overload. There are some constraints on the arguments and the return types (and the constraints differ depending on the operator being overloaded), but for the most part you can treat them like standard method arguments and return types. The contents can be any sort of calculation - as long as what is returned matches the return type.

For overloading binary operators, the only difference is that the operator function takes two arguments. Here, we are overloading the addition operator:
class SomeClass
{
    private int someValue;

    public SomeClass(int val)
    { someValue = val; }

    public static SomeClass operator +(SomeClass arg1, SomeClass arg2)
    {
         return new SomeClass(arg1.someValue + arg2.someValue);
    }
}
 
And of course now you can now use this overload like the following:
SomeClass var1 = new SomeClass(1);
SomeClass var2 = new SomeClass(2);
SomeClass var3 = var1 + var2;
 
And now var3.someValue holds the value 3.

Now that you know the syntax for overloading operators, you might be wondering what operators you are allowed to overload. Well, with C#, the answer is almost every single one. For binary operators, you can overload any of the following:
+, -, !, \~, ++, --, true, false

Most of those probably make sense, but what does it mean to overload true or false? Well, first, take a look at the following implementation:
class SomeClass
{
    private int someValue;

    public SomeClass(int val)
    { someValue = val; }

    public static bool operator true(SomeClass arg1)
    { return arg1.someValue >= 0 }

    public static bool operator false(SomeClass arg1)
    { return arg1.someValue < 0 }
}
 
You can't overload only true or false - you have to overload them both if you want one (which makes sense - how could you even mentally define one without defining the other).

And this is how it can be used:
SomeClass var1 = new SomeClass(1);
if(var1)
{
  //do something
}
 
var1 would be treated as true, because the true operator returned true. I don't actually think overloading the operators true and false is very useful - I think a much better way to get the same functionality is to create an operator overload to implicitly cast your object to a boolean (we will get into how to do that later on).

Back to what you can actually overload. For binary operators, you can overload the following:
+, -, *, /, %, &, |, \^, \<\<, >>

All of those should make sense, so I'll move on to comparison operators:
==, !=, \<, >, \<=, >=

The only caveat here is that these operators need to be overloaded in pairs - i.e., if you overload ==, you must overload !=. This makes sense, for the same reason that you must overload true and false as a pair.

You can't directly overload && or || - but not to worry, you can get an overloaded 'effect' by overloading either the true and false operators, or by defining an implicit cast to boolean.

The same goes for all the 'assignment plus operator' operators:
+=, -=, *=, /=, %=, &=, |=, \^=, \<\<=, >>=

You can't overload them directly, but if you think about it, every one of them is a combination of = and some operator that you can overload - and so by overloading, say, +, you end up overloading +=.
You can't overload any of these at all:
=, ., ?:, ->, new, is, sizeof, typeof

But please, I would hope you didn't actually want to do that in the first place.

One cool operator that doesn't fit in any of these categories is the index operator: []

You can actually 'overload' it (Microsoft doesn't call it overloading - they call it 'declaring an indexer'), and it comes in handy sometimes. It doesn't use operator overload syntax - instead, it uses the C# property syntax. For instance:
class SomeClass
{
    private int[] someValues = new int[3]{1,2,3};

    public int this [int index]
    {
      set{ someValues[index] = value; }
      get{ return someValue[index]; }
    }
}
 
And now you can use it like this:
SomeClass var1 = new SomeClass();
var1[0] = var1[2];
 
And after that operation, the array in var1 looks like: {3,2,1}.

There is one other set of overloading operations to talk about before we start pushing operator overloading to its limits - and that is cast overloading. There are two types of cast overloads: implicit and explicit. Explicit casting is when you write code like:
double val = 4.555;
int intVal = (int)val;
 
That (int) is an explicit cast.
Implicit casting is what occurs in the following situation:
int intVal = 1;
double val = intVal;
 
Here, the int intVal is implicitly casted to a double - you as the programmer don't even have to think about it.

So lets take a look at some cast overloads:
class SomeClass
{
    private int someValue;

    public SomeClass(int val)
    { someValue = val; }

    public static implicit operator bool(SomeClass arg1)
    {
         return arg1.someValue >=0;
    }

    public static explicit operator int(SomeClass arg1)
    {
       return arg1.someValue;
    } 
}
 
Here, we have an implicit overload for casting to bool, and an explicit overload for casting to int. The syntax is close to other operator overloads, but not quite the same. We don't have to declare a return type in the normal spot, because the name of the cast is the return type.

Now that we have these casts defined, they can be used like this:
SomeClass var1 = new SomeClass(1);
int val = -1;
if(var1)
  val = (int)var1;
 
Here, var1 will evaluate to true in the if, thanks to the implicit boolean cast. And then, val will get the value 1, because of the explicit int cast.

But enough with the simple stuff - what happens if we do the unexpected with operator overloading?
Some Strange Arguments 
 
So operator overloads are just methods, right? So that means we should be able to do all sorts of crazy things with them. Sadly (or fortunately, depending on your perspective) that is not the case. For instance, you can't use the keywords ref or out with the arguments. For unary operators (like ++ and --) both the argument and the return type have to be the class that the overload is declared for. So that means that the following won't compile:
class SomeClass
{
    public static SomeClass operator ++(int arg1)
    { return new SomeClass(); }
}
//Error: The parameter type for ++ or -- operator must be the containing type
 
or this:
class SomeClass
{
    public static int operator ++(SomeClass arg1)
    { return new 0; }
}
//Error: The parameter type for ++ or -- operator must be the containing type
 
But it makes sense that that wouldn't compile, because the 'input' and the 'output' of ++ when your actually using it in code need to be the same type.

Don't lose hope, though - because you can do odd things with the arguments and return types of binary operators. For instance, this is legal:
class SomeClass
{
    public static int operator +(SomeClass arg1, int arg2)
    { return new 0; }
}
 
And will work in the following statement:
SomeClass var1 = new SomeClass();
int i = var1 + 2;
 
However it will not work with:
int i = 2 + var1;
 
In this second case it will actually try and add var1 as an integer to '2' - i.e., use the regular integer '+'. This is because the type of the first argument to the overload function needs to match the type of the value before the '+', and the second argument type has to match what is after the '+'. To get it to work in both directions you need to write two functions:
class SomeClass
{
    public static int operator +(SomeClass arg1, int arg2)
    { return new 0; }
    public static int operator +(int arg1, SomeClass arg2)
    { return new 0; }
}
 
Be aware that declaring overloads like this has the potential to mess with your use of the 'assignment plus operator' operators, like +=. In this case, because of how we declared these overloads, the following code will cause a compiler error:
SomeClass var1 = new SomeClass();
var1 += 5;
//Error: Cannot implicitly convert type 'int' to 'SomeClass'
 
It throws this error because the overloaded operator is returning an int, and we are trying to place it into a SomeClass variable.

One thing to note - the type of at least one of the arguments to the overload function must be the type of the class that you are declaring it in. So something like the following will not compile:
class SomeClass
{
    public static SomeClass operator +(int arg1, int arg2)
    { return null; }
}
//Error: One of the parameters of a binary operator must be the containing type
 
Another thing to be aware of - you don't want to declare the 'same' overload in two different classes. By the 'same', I mean two overloads that have identical signatures. For instance, the following won't compile:
class OneClass
{
    public static OneClass operator +(OneClass arg1, 
        TwoClass arg2)
    { return null; }
}
class TwoClass
{
    public static OneClass operator +(OneClass arg1, 
        TwoClass arg2)
    { return null; }
}
//Error: The call is ambiguous between the following methods or properties: 
'OneClass.operator +(OneClass, TwoClass)' and 'TwoClass.operator +(OneClass, TwoClass)'

It won't compile because the compiler can't figure out which of the overload methods to use.

That covers it for part one of crazy operator overloading. Check back soon for part two, where we will deal with polymorphism and inheritance, as well a number of other weird twists which no one in their right mind would use in their code.

Comments