For a project I’ve been working on, I had to do some spelunking on C#’s operator overloading support. I discovered some interesting things about the equality and increment operators and how they are handled by the C# compiler in classes that do and do not overload them. I’ll be sharing the code soon, but I figured I’d share the learning right away.
There are two main mechanisms for testing equality in the CLR: the System.Object.Equals method and the equality operator (i.e. the double equal sign ==). Every object in CLR has an Equals() method that it inherits from System.Object. The default implementation of this method uses bitwise equality for value types and referential equality for reference types. The equality operator maps to the IL opcode “ceq” (check equality) which has similar semantics to the Equals() method. However, the equality operator only works with native value types and referential types. You can’t use the equality operator with value types that you define (i.e. structs in C#) unless you create an overload for it.
For some referential types, the default behavior is inappropriate. For example, even though System.String is a referential type, its Equals() method has been overridden to provide value type semantics. So when you compare two strings that have the save character values, Equals() returns true even if they are two different string instances. Since Equals() is a virtual function, it gets overridden in the standard fashion. However, the important thing to keep in mind is that while Equals() and the equality operator have similar semantics, they are not the same operation. You override and implement them separately. If you override the equality operator in your class, the C# compiler generates a call to that method instead of a ceq IL opcode. If you want Equals() and the equality operator to yield the same result, you can call one from the other. For example, the overloaded equality operator for System.String calls the overridden Equals() method under the hood.
Having to semantically similar but separately overridable mechanisms for testing equality can get sticky. Not all languages support operator overloading. If I’m using VB.NET, there’s no way to override the equality operator (VB.NET uses a single = instead of C#’s double ==). It gets worse in cross language scenarios. I can consume a C# object that overrides Equals() and equality from VB.NET with no problem, but if I try to go the other way, the equality operator ends up with potentially different semantics than the Equals() method. This must be why, according to MSDN, it is recommended that most reference types not override the equality operator, even if they override Equals(). This way, you just get into the habit of always using Equals() I guess. Personally, I do all my work in C# so I decided to take full advantage of the language and overload the equality operator of my class to call Equals() under the hood, just like System.String.
 Actually, String.Equals() is implemented in native code by the runtime for performance reasons.
One of the tricky aspects about the increment operator (i.e. the double plus sign ++) is the difference between prefix and postfix notation. Both versions of the increment operator increment the current value and return a result. The difference between the two versions is the order in which these operations are applied. In prefix (i.e. ++foo), the value is incremented and then the incremented value is returned as the result. In postfix (i.e. foo++), the original value is saved, the value is incremented, and then the original value is returned as the result. In both cases, the value of the variable is the same after the operation is completed. C# supports both prefix and postfix notations as well as overloading the ++ operator. What it doesn’t support, however, is having different implementations of the prefix and postfix operations.
In C++, the prefix and postfix versions overloads are specified separately, though there is a template that provides a standard implementation of the postfix version in terms of the prefix version. In pseudo-code, it reads “Save old version, call prefix increment operation, return saved version”. In C#, this pseudo-code is generated by the compiler, not the overloaded operator implementation. This means you only build the single increment operation and the C# compiler generates the appropriate IL to handle the pre- and post fix scenarios.
For example, if I have an integer i and I call Console.Write(++i), the C# compiler generates the following IL:
ldloc.0 //load i variable onto the stack ldc.i4.1 //load the value 1 onto the stack add //add the two values on top of the stack dup //duplicate the value on the top of the stack //This stack item gets passed to Console.Write stloc.0 //store the result of the addition call Console.Write
And if I call Console.Write(i++):
ldloc.0 //load i variable onto the stack //This stack item gets passed to Console.Write dup //duplicate the value on the top of the stack ldc.i4.1 //load the value 1 onto the stack add //add the two values on top of the stack stloc.0 //store the result of the addition call Console.Write
As you can see, the incremented value is always stored back into the variable i (loc.0 in the IL). However, what is left on the stack differs depending on the pre- or postfix version. In prefix, the value is incremented then duplicated. One duplicate is saved back to the variable while the other is passed to Console.Write. In postfix, it is duplicated then incremented. The incremented version is saved back to the variable while the original value is passed to Console.Write.
The same rules apply for classes that override the increment operator. In that case, a call to the overloaded increment operator instead of directly loading 1 onto the stack and calling the add operand. Also, the same rules apply for decrement (i.e. double minus sign –).