Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#
Article

Simulating polymorphic operator overloads with C#

,
Rate me:
Please Sign up or sign in to vote.
4.67/5 (21 votes)
19 Apr 2005CPOL4 min read 152.5K   23   25
Article shows how to overcome the C# insistence on operator overloads being static and describes a method for simulating polymorphic behavior for operator overloads.

Overview

Occasionally this question pops up in newsgroups and forums : Why does C# insist on operator overloads being static? The person raising the question also usually complains how this prevents him or her from implementing virtual overloaded operators.

This article explains why operator overloads have to be static in C# (or any other MSIL compiler), and also shows how you can simulate virtual overloaded operators very easily. It's not a universe-shattering theory (nor a very original one for that matter) and uses a very simple pattern though it's this very simplicity that makes it interesting.

So, why do they have to be static?

In C#, GC'd objects are heap-allocated (the managed CLR heap, not the CRT heap) and thus GC'd objects are always used as references (into the CLR heap). You cannot have stack-based GC'd objects in C# and what this means that you never know when an object variable is null. So, you can imagine what happens if operator overloads were instance methods and you tried to use an operator on a null object! Traditional C++ (as opposed to the CLI version) never faced this problem because the operators were always applied on stack objects; and if at all pointers were used, since pointers followed their own set of operational behavior - you never face a situation where an overloaded op-overload method is invoked on an invalid object.

Traditional C++ example

See below some C++ code that uses virtual operator overloads :-

class Base
{
public:
    Base(int x):x_value(x){}
    virtual bool operator ==(const Base& b)
    {
        return x_value == b.x_value;
    }
protected:
    int x_value;
};

class Derived : public Base
{
public:
    Derived(int x, int y): Base(x), y_value(y){}    
    virtual bool operator ==(const Base& b)
    {        
        Derived* pD = (Derived*)&b;
        return (pD->y_value == y_value) && (pD->x_value == x_value);
    }
private:
    int y_value;
};

int _tmain(int argc, _TCHAR* argv[])
{
    Base* b1 = new Derived(2,11);
    Base* b2 = new Derived(2,11);

    cout << (*b1==*b2) << endl;

    return 0;
}

When (*b1==*b2) is evaluated the == operator overload for class Derived is invoked which can be easily verified by trying out different values for the Base constructors for b1 and b2 or by setting a breakpoint inside the operator overload.

A port to C#

Taking what we know of C# let's attempt a straight-forward port of the above example to C#.

C#
public class Base
{
    int x;

    public Base( int x )
    {
        this.x = x;
    }

    public static bool operator==( Base l, Base r )
    {
        if( object.ReferenceEquals( l, r ) )
            return true;
        else if( object.ReferenceEquals( l, null ) || 
                 object.ReferenceEquals( r, null ) )
            return false;
            
        return l.x == r.x;
    }

    public static bool operator!=( Base l, Base r )
    {
        return !(l == r);
    }

    public int X { get { return x; } }
}

public class Derived : Base
{
    int y;

    public Derived( int x, int y ) : base( x )
    {
        this.y = y;
    }

    public static bool operator==( Derived l, Derived r )
    {
        if( object.ReferenceEquals( l, r ) )
            return true;
        else if( object.ReferenceEquals( l, null ) || 
                 object.ReferenceEquals( r, null ) )
            return false;
        
        return (l.y == r.y) && (l.X == r.X);
    }

    public static bool operator!=( Derived l, Derived r )
    {
        return !(l == r);
    }

    public int Y { get { return y; } }
}

class Program
{
    static void Main()
    {
        Derived d1 = new Derived( 2, 11 );
        Derived d2 = new Derived( 2, 11 );

        Console.WriteLine( d1 == d2 );
        Console.ReadLine();
    }
}

If we run the program as it is above everything will work like the C++ version, but if we introduce a slight change to the program things begin to deviate greatly.

C#
class Program
{
    static void Main()
    {
        Base d1 = new Derived( 2, 11 );
        Base d2 = new Derived( 2, 12 );

        Console.WriteLine( d1 == d2 );
        Console.ReadLine();
    }
}

What's going on here? As simple debugging will show us the despite the objects being compared being instances of the Derived class the Base class == operator is being called. This is because C# (and thus, most other languages) figure out which == operator method to call based on the known (i.e. compile-time) type of the object on the left hand side of the operation.

There are ways around this as you'll see below.

Simulating operator polymorphism with C#

Here, we see how to simulate this in C# :-

C#
class Base
{
    protected int x_value = 0;    

    public Base(int x)
    {
        x_value = x;
    }

    public static bool operator==(Base b1, Base b2)
    {
        if( object.ReferenceEquals( b1, b2 ) )
        {
            return true;
        }
        else if( object.ReferenceEquals( b1, null ) || 
                 object.ReferenceEquals( b2, null ) )
        {
            return false;
        }
        
        return b1.Equals(b2);            
    }

    public static bool operator !=(Base b1, Base b2)
    {
        return !(b1 == b2);
    }

    public override bool Equals(object obj)
    {
        if( obj == null )
            return false;
        
        Base o = obj as Base;
        
        if( o != null )    
            return x_value == o.x_value;
        return false;
    }


    public override int GetHashCode()
    {
        return x_value.GetHashCode();
    }
}

class Derived : Base
{
    protected int y_value = 0;
    

    public Derived(int x, int y) : base(x)
    {
        y_value = y;
    }

    public override bool Equals(object obj)
    {
        if( !base.Equals( obj ) )
          return false; 
          
        Derived o = obj as Derived;
        
        if( o == null )
            return false;
        
        return y_value == o.y_value;
    }

    public override int GetHashCode()
    {
        return x_value.GetHashCode() ^ y_value.GetHashCode() + x_value;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Base b1 = new Derived(10, 12);
        Base b2 = new Derived(10, 11);
        
        Console.WriteLine(b1 == b2);

        b2 = null;

        Console.WriteLine(b1 == b2);

        Console.ReadKey(true);
    }
}

Rather than rely on the == operator overload to do all of the heavy lifting we push all of the work onto the virtual Equals method, from there we let polymorphism work its magic.

Points to note

  • The operator overload has to be static, so we have virtual instance methods that implement the logic for us and we invoke these virtual methods from the static operators

  • In our example, the method Equals corresponds to == and Equals is a virtual method (inherited from System.Object)

  • Within the static overload we need to check for null (to avoid null-reference exceptions)

  • Within each derived class's corresponding operator-logic method (Equals in our case), we cast the

    C#
    System.Object 
    argument to the type of the class (e.g. - In the Derived class we cast to Derived)

  • While Equals and == already exist in System.Object, we can implement similar methods for any operator in our class hierarchies. Say, we need to implement the ++ operator, we then add a PlusPlus (or Increment) virtual method to the root base class in our object hierarchy and in the ++ overload we invoke the Increment method on the passed-in object.

  • In our example, we check for null and return true or false depending on whether the objects are both null or not. But, if you are implementing an operator like ++ then you might want to check for null and throw an ArgumentNullException (to override the inappropriate NullReferenceException that'd otherwise get thrown).

History

  • Apr 19, 2005 : Article first published
  • Apr 20, 2005 : Fixed a bug in Derived.Equals and also changed the GetHashCode implementations for Base and
    Derived
    (thanks to Jeffrey Sax for pointing this out)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Written By
Software Developer (Senior) InfoPlanIT, LLC
United States United States
James has been programming in C/C++ since 1998, and grew fond of databases in 1999. His latest interest has been in C# and .NET where he has been having fun writing code starting when .NET v1.0 was in its first beta.

He is currently a senior developer and consultant for InfoPlanIT, a small international consulting company that focuses on custom solutions and business intelligence applications.

He was previously employed by ComponentOne where he was a Product Manager for the ActiveReports, Data Dynamics Reports, and ActiveAnalysis products.

Code contained in articles where he is the sole author is licensed via the new BSD license.

Comments and Discussions

 
Generalthanks Pin
Member 313705328-Aug-08 15:41
Member 313705328-Aug-08 15:41 
GeneralGreat but what about interfaces Pin
Lofvall4-Aug-05 2:17
Lofvall4-Aug-05 2:17 
GeneralThis article is good but... Pin
thelazydogsback28-Apr-05 5:53
thelazydogsback28-Apr-05 5:53 
is a perfect example of why operator overloading is a bad idea:

- The complications are not worth the little bit of syntactic sugar that you gain.
- Implementing it correctly can be complex, leading to bugs
- It's hard for others to read your code - you need a global view of the code before you know what operators do.
- Language supplied operators should be atomic or at least simple -- who needs "[]" to format their hard-drive? Or at least, in a code review, if I see for( i...) { x=y[i] } I don't expect thousands of cycles to be spent in "[]".
- There are only a limmited # of operators to overload with obvious symantics anyway - so you may as well start with descriptive names in the first place, rather than "==" have one meaning and then "MyEquals" doing the next.

Of all the operators, "[]" is the only one I've overriden -- but is x[7] really so much better than x.At(7) that it's worth any risks? Infix operators such as == are a little prettier than x.Equals(y) -- but I think == should be a primitive and always mean the same thing (object equivilence). I would rather have seen the designers of C# provide an operator for object equality ("===" and "!==" let's say) and have that operator be overloadable.

I for one am glad that C# is not C++.
Just my $0.02...
thanks

QuestionUsing Object.Equals static method? Pin
Anonymous27-Apr-05 22:31
Anonymous27-Apr-05 22:31 
GeneralOnly works inside your own hierarchy. Pin
tafkat27-Apr-05 12:43
tafkat27-Apr-05 12:43 
GeneralGetHashCode Pin
Jeffrey Sax19-Apr-05 3:51
Jeffrey Sax19-Apr-05 3:51 
GeneralRe: GetHashCode Pin
leppie19-Apr-05 5:30
leppie19-Apr-05 5:30 
GeneralRe: GetHashCode Pin
James T. Johnson19-Apr-05 7:14
James T. Johnson19-Apr-05 7:14 
GeneralRe: GetHashCode Pin
Jeffrey Sax19-Apr-05 7:45
Jeffrey Sax19-Apr-05 7:45 
GeneralRe: GetHashCode Pin
James T. Johnson19-Apr-05 20:03
James T. Johnson19-Apr-05 20:03 
GeneralRe: GetHashCode Pin
Jeffrey Sax21-Apr-05 2:20
Jeffrey Sax21-Apr-05 2:20 
GeneralRe: GetHashCode Pin
Nish Nishant19-Apr-05 19:30
sitebuilderNish Nishant19-Apr-05 19:30 
GeneralGreat article Pin
S. Senthil Kumar19-Apr-05 0:37
S. Senthil Kumar19-Apr-05 0:37 
GeneralRe: Great article Pin
Nish Nishant19-Apr-05 1:03
sitebuilderNish Nishant19-Apr-05 1:03 
GeneralRe: Great article Pin
S. Senthil Kumar19-Apr-05 4:44
S. Senthil Kumar19-Apr-05 4:44 
GeneralRe: Great article Pin
James T. Johnson19-Apr-05 7:19
James T. Johnson19-Apr-05 7:19 
GeneralRe: Great article Pin
Nish Nishant19-Apr-05 19:28
sitebuilderNish Nishant19-Apr-05 19:28 
GeneralTypos :p Pin
leppie18-Apr-05 22:59
leppie18-Apr-05 22:59 
GeneralRe: Typos :p Pin
Nish Nishant19-Apr-05 1:06
sitebuilderNish Nishant19-Apr-05 1:06 
QuestionWhy not? Pin
Martin Holzherr18-Apr-05 21:07
Martin Holzherr18-Apr-05 21:07 
AnswerRe: Why not? Pin
Nish Nishant19-Apr-05 1:07
sitebuilderNish Nishant19-Apr-05 1:07 
GeneralRe: Why not? Pin
S. Senthil Kumar19-Apr-05 3:12
S. Senthil Kumar19-Apr-05 3:12 
AnswerRe: Why not? Pin
Jeffrey Sax19-Apr-05 3:43
Jeffrey Sax19-Apr-05 3:43 
GeneralRe: Why not? Pin
S. Senthil Kumar19-Apr-05 4:41
S. Senthil Kumar19-Apr-05 4:41 
GeneralRe: Why not? Pin
Nish Nishant19-Apr-05 19:37
sitebuilderNish Nishant19-Apr-05 19:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.