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

Operator Overloading with Generics

Rate me:
Please Sign up or sign in to vote.
4.67/5 (15 votes)
23 Jun 20054 min read 93.3K   418   34   13
Using Lightweight Code Generation and delegates to allow operator overloading in .NET 2.0.

Introduction

As Rüdiger Klaehn explains in his article, Using Generics for calculations, attempting to use the +, -, etc. operators with generic types doesn't work. Not directly, at least. The current literature has favored a rather convoluted approach, well-described in Klaehn's article.

However, there exists another approach, much cleaner, which runs on average at least as fast as the equivalent non-generic version. Following is a brief explanation of my solution.

Lightweight Code Generation

Lightweight Code Generation (LCG) is a new facility in .NET 2.0 which allows for the creation of method delegates at runtime. It's lighter-weight than the traditional runtime creation in that you don't have to create entire classes. IronPython is a notable example of LCG at work.

Klaehn's article cites a UseNet post by Daniel O'Connell, showing how one would call the addition operator using ILGenerator.Emit(). However, other posters in the thread become concerned about inflexibility and aesthetics, while Klaehn himself expressing concern about the speed of invocation.

There is, indeed, a price to be paid for the use of late-bound methods. In .NET 1.x, the normal Invoke() mechanisms were relatively slow and painful. However, in 2.0, delegates have been optimized such that, if called correctly, they can be nearly as fast as a normal call. In the July 2005 issue of MSDN, in fact, Joel Pobar discusses calling speed in his article, Reflection: Dodge Common Performance Pitfalls to Craft Speedy Applications.

The Code

My solution provides a BinaryOperator delegate and a static GenericOperatorFactory, both generic.

C#
public delegate TResult BinaryOperator<TLeft, TRight, 
          TResult>(TLeft left, TRight right);

static class GenericOperatorFactory<TLeft, TRight, TResult, TOwner>
{
    private static BinaryOperator<TLeft, TRight, TResult> add;
    public static BinaryOperator<TLeft, TRight, TResult> Add
    {
        get { ... }
    }
}

As you can see, the class defines an Add property. The getter is defined as follows:

C#
public static BinaryOperator<TLeft, TRight, TResult> Add
{
    get
    {
        // if we haven't created the delegate yet, do so now
        if (add == null)
        {
            Console.WriteLine(@
                "Creating Add delegate for:
                TLeft = {0}
                TRight = {1}
                TResult = {2}
                TOwner = {3}", 
                typeof(TLeft), 
                typeof(TRight), 
                typeof(TResult), 
                typeof(TOwner)
            );

            // create the DynamicMethod
            DynamicMethod method = 
                new DynamicMethod(
                    "op_Addition" 
                    + ":" + typeof(TLeft).ToString() 
                    + ":" + typeof(TRight).ToString() 
                    + ":" + typeof(TResult).ToString() 
                    + ":" + typeof(TOwner).ToString(),
                    typeof(TLeft),
                    new Type[] { 
                        typeof(TLeft), 
                        typeof(TRight) 
                    },
                    typeof(TOwner)
                );

            ILGenerator generator = method.GetILGenerator();

            // generate the opcodes for the method body
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldarg_1);

            if (typeof(TLeft).IsPrimitive)
            {
                // if we're working with a primitive, 
                // use the IL Add OpCode
                generator.Emit(OpCodes.Add);
            }
            else
            {
                // otherwise, bind to the definition 
                // with the given type
                MethodInfo info = typeof(TLeft).GetMethod(
                    "op_Addition",
                    new Type[] { 
                        typeof(TLeft), 
                        typeof(TRight) 
                    },
                    null
                );

                generator.EmitCall(OpCodes.Call, info, null);
            }

            // insert a return statement
            generator.Emit(OpCodes.Ret);

            Console.WriteLine("Method name = " + method.Name);


            // store the delegate for later use
            add = (BinaryOperator<TLeft, TRight, TResult>) 
            method.CreateDelegate(typeof(BinaryOperator<TLeft, TRight, TResult>));
        }

        return add;
    }
}

The code generation is actually very simple. It's also generic: the types of the left and right sides of the operation, as well as the return type and owner of the method are all parameterized. Since the CLR creates separate specializations of this generic class as it's called at runtime, there's no need for a Hashtable or the like to store different versions of the Add delegate: the CLR will call the version of GenericOperatorFactory specified by the type parameters supplied.

Usage

Usage is an important consideration. Obnoxious syntax will prevent use, and incorrect use will lead to performance problems. Here's what I've got:

C#
public class Foo<T>
{
    public T Value;

    public Foo(T newValue)
    {
        this.Value = newValue;
    }

    /// <summary>
    /// cached copy of the Add<T,T> delegate
    /// </summary>
    private static BinaryOperator<T, T, T> addTT;

    /// <summary>
    /// overloaded addition operator
    /// This will use GenericOperatorFactory to create 
    /// the Add<T,T> delegate
    /// </summary>
    /// <param name="p1"></param>
    /// <param name="p2"></param>
    /// <returns></returns>
    public static Foo<T> operator +(Foo<T> p1, Foo<T> p2)
    {
        // use addTT to cache the delegate locally
        if (addTT == null)
        {
            addTT = GenericOperatorFactory<T, T, T, Foo<T>>.Add;
        }

        return new Foo<T>(addTT(p1.Value, p2.Value));
    

        // use GenericOperatorFactory's cached version (slower)
        // return new Foo<T>(GenericOperatorFactory<T, T, T, Foo<T>>
        //            .Add(p1.Value, p2.Value));
    }
}

If you've seen invocations of runtime-generated code before, you've probably seen some form of Delegate.Invoke(...), and noted it took quite a while to call the method (during testing, it was running about 130x slower than a non-generic equivalent). Note, however that I'm storing the generated delegate in a private delegate member and calling it normally. There would be, I imagine, one per permutation of types in the operator. Another option, though slower, would be to use the GenericOperatorFactory's cached copy. Again, there's a tradeoff between elegance and speed.

The class can now be instantiated and used:

C#
Foo<int> fooInt1 = new Foo<int>(1);
Foo<int> fooInt2 = new Foo<int>(2);
 
Foo<int> result = fooInt1 + fooInt2; // ok
Foo<int> error1 = fooInt1 + 2;  // Error 1 Operator '+' cannot be applied 
                // to operands of type 'GenericOperators.Foo<int>'
                // and 'int'

Compare this syntax to that in Using Generics for calculations, where there are several different interfaces to use:

C#
class Lists<T,C>
    where T:new()
    where C:ICalculator<T>,new()
{
    public static T Sum(List<T> list)
    {
        Number<T,C> sum=new T();
        for(int i=0;i<list.Count;i++)
            sum+=list[i];
        return sum;
    }
}

Performance

I've included a small test program which compares:

  • a tight int + int loop;
  • a FooInt + FooInt loop, where FooInt is a non-generic class defining an operator+(int, int) overload;
  • a Foo<int> + Foo<int> loop, using the Foo<T> generic described above;
  • a Foo<int> - Foo<int> loop.

On my system, I get fairly consistent results while running the test program:

int = 1783293664; delta: 156254
FooInt = 1783293664; delta: 1406286
Foo<int> add = 1783293664; delta: 1093778
Foo<int> subtract = -1783293664; delta: 1250032
ratio add vs int = 7
ratio add vs FooInt = 0.777777777777778
ratio sub vs add = 1.14285714285714

"delta" is the elapsed ticks from start to end of the loop.

int + int is of course faster than Foo<int>, but only by 7x. However, Foo<int> is roughly 30% faster than FooInt (1/0.77 ~ 1.3). Repeated execution shows some variation -- occasionally large -- but the values tend to live somewhere near what is shown here.

What surprised me at this point is that the Foo<int> speeds are generally faster than FooInt. I've examined the IL, and I don't see why this should be. Someone out there, please enlighten me on the subject.

The relative speed of this code has not been tested against Klaehn's.

Conclusion

I hope this code proves itself a useful basis for learning about Lightweight Code Generation, and removes some of the latent anxieties people seem to have regarding dynamic invocation and generic calculations in general. Included are addition and subtraction; the other operators are left as an exercise to the developer. Until and unless Microsoft creates a fast-tracked mechanism for overloading operators in generic classes, we're left with such work-arounds.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior) Idea Entity
United States United States
I taught myself, programming BASIC in 1982 on an Atari 400, and went on from there.

I was a developer on version 1 of LINQ to SQL; now I work for Idea Entity on other Microsoft projects.

Comments and Discussions

 
GeneralMy vote of 5 Pin
abhishek123a@gmail.com6-Dec-11 0:53
abhishek123a@gmail.com6-Dec-11 0:53 
GeneralWould this work.. [modified] Pin
Flandhart5-Jun-06 10:09
Flandhart5-Jun-06 10:09 
GeneralChanges to test Pin
bistok11-Oct-05 15:28
bistok11-Oct-05 15:28 
GeneralGetting rid of the &quot;if&quot; statement Pin
beep1-Sep-05 7:07
beep1-Sep-05 7:07 
GeneralRe: Getting rid of the &quot;if&quot; statement Pin
beep1-Sep-05 7:08
beep1-Sep-05 7:08 
GeneralRe: Getting rid of the &quot;if&quot; statement Pin
Keith Farmer1-Sep-05 8:06
Keith Farmer1-Sep-05 8:06 
GeneralRe: Getting rid of the &quot;if&quot; statement Pin
Keith Farmer1-Sep-05 8:24
Keith Farmer1-Sep-05 8:24 
GeneralHowever, Foo&lt;int&gt; is roughly 30% faster than FooInt Pin
smiddleton19-Aug-05 8:36
smiddleton19-Aug-05 8:36 
GeneralRe: However, Foo&lt;int&gt; is roughly 30% faster than FooInt Pin
Keith Farmer19-Aug-05 9:24
Keith Farmer19-Aug-05 9:24 
Generalgeneric static member pattern Pin
umuhk7-Jul-05 10:38
umuhk7-Jul-05 10:38 
GeneralPerformance values Pin
James Curran24-Jun-05 4:35
James Curran24-Jun-05 4:35 
GeneralRe: Performance values Pin
Keith Farmer24-Jun-05 6:25
Keith Farmer24-Jun-05 6:25 
GeneralRe: Performance values Pin
Keith Farmer24-Jun-05 12:43
Keith Farmer24-Jun-05 12:43 

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.