Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Operator Overloading with Generics

0.00/5 (No votes)
23 Jun 2005 1  
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.

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:

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:

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:

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:

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