Click here to Skip to main content
Click here to Skip to main content

Generic Operators / Numerics in C#

, 31 Oct 2013
Rate this:
Please Sign up or sign in to vote.
How to create 'generic' classes and methods which use operators such as +,-, / and *

Introduction 

In C#, you cannot create generic methods, or classes, for objects based on their operators (such as +). For example, the following will not compile:

public static T Add<T>(T a, T b)
{
   return a+b;
}    

This can be a real problem when dealing with data that varies in type, but not (or only slightly) in behaviour. For example, images are commonly stored as bytes (0-255), 16-bit unsigned integers (0-65k), and even as floating point variables. Matrices, similarly, may hold integer-, double-, or decimal-data. However, in C#, there is no simple way to write a generic method or class to process such data.

While some solutions to this problem have been proposed (e.g. http://www.codeproject.com/Articles/8531/Using-generics-for-calculations), these solutions typically require wrappers, helper classes, unintuitive code, and/or poorer performance.

Background

When a generic class is used in a .NET application, it is created as needed, and is as 'real' and unique as any other class in the application. For example, take the generic class Collar<T>:

public class Collar<T>
{
   T Animal{get;set;}
   public Collar(T a)
   {
       Animal = a;
   }                          
}

If we create a Collar<Dog> and a Collar<Cat> in our application, the compiler will create the types Collar<Dog> and a Collar<Cat> as distinct types. That is to say, that while they may have a similar behaviour and share code (as we see it), they are completely different types from one another (as the runtime sees it).

The reason that we cannot create generic types or generic methods for numerical operations, is that we cannot currently use interfaces to specify operators, such as +, -, /, and *, and we cannot require them using a where clause like so:

public class Image<T> where T:+,-,*,/ //illegal
{}

The Solution

The solution to this problem is a custom pre-processing step which utilises a class 'template', very similar to a generic definition, to build pre-specified types that you can use in your code. It injects the code for these types into your project directly, before compilation takes place. These templated types only differ from generics in that they:

  • Are named slightly differently (e.g. Calc_Double, rather than Calc<Double>)
  • Are specified prior to compilation
  • Have the ability to use operators

In the first example below, I will automatically generate Image<T> objects, one of which will hold pixel values as Bytes, one which will hold ints, and another which will hold floats. The images will have a GetPixel, SetPixel, and MaxPixel methods. The second example creates a single class that has mathematical methods for doubles, ints, and floats.

Using the Code

Add the Preprocessor

The first step is to tell VS to use the preprocessor attached to this article. Open your .csproj file in Notepad, and scroll to the bottom, where you will see commented text like so:

  <!-- To modify your build process, 
add your task inside one of the targets below and uncomment it. 
Other similar extension points exist, see Microsoft.Common.targets. 
<Target Name="BeforeBuild"></Target>
<Target Name="AfterBuild"></Target>  
-->     

Delete this comment and write the following:

<Target Name="BeforeBuild">
<Exec WorkingDirectory="FOLDER_OF_PREPROCESSOR"
Command="GenericNumericsPreprocessor.exe &quot;$(MSBuildProjectDirectory)&quot; 
&quot;$(MSBuildProjectFullPath)&quot;" />
</Target>      

Where FOLDER_OF_PREPROCESSOR is the directory that holds the built the preprocessor executable (GenericNumericsPreprocessor.exe). You can also paste this executable into the project folder of your project, and leave WorkingDirectory as an empty string.

Add <T> Placeholder to your Project

As we have seen, we cannot use generics to make methods that use operators such as +, - or * for datatypes such as int or float. Instead, we will use a <T> PlaceHolder type (TPH), which specifies every operator and conversion that you wish. When the file is preprocessed, everywhere that holds this operator will be replaced by the types that you wish.

As this placeholder specifies all operators we wish to use, it will not result in intellisense-level compilation errors. An example placeholder is in the project attached to this article. It looks like so:

public class TPH : IEquatable<TPH>, IComparable<TPH>
{
    #region OPERATORS
 
    public static bool operator !=(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static bool operator <(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static bool operator <=(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static bool operator ==(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static bool operator >=(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static bool operator >(TPH c1, TPH c2)
    {
        return false;
    }
 
    public static TPH operator +(TPH c1, TPH c2)
    {
        return null;
    }
    public static TPH operator -(TPH c1, TPH c2)
    {
        return null;
    }
    public static TPH operator *(TPH c1, TPH c2)
    {
        return null;
    }
    public static TPH operator /(TPH c1, TPH c2)
    {
        return null;
    }
    public static TPH operator ++(TPH c1)
    {
        return null;
    }
 
    public static TPH operator --(TPH c1)
    {
        return null;
    }
 
    #endregion
 
    #region Conversions
 
    public static implicit operator TPH(byte d)
    {
        return null;
    } 

    public static explicit operator TPH(short d)
    {
        return null;
    }
 
    public static implicit operator TPH(int d)
    {
        return null;
    } 

    public static explicit operator TPH(long d)
    {
        return null;
    }
 
    public static explicit operator TPH(float d)
    {
        return null;
    }
 
    public static explicit operator TPH(double d)
    {
        return null;
    } 

    #endregion
 
    #region IComparable<ExpandDud> Members
 
    int IComparable<TPH>.CompareTo(TPH other)
    {
        return 0;
    }
 
    #endregion
 
    #region IEquatable<ExpandDud> Members
 
    bool IEquatable<TPH>.Equals(TPH other)
    {
        return false;
    }
 
    #endregion
 
    public override bool Equals(object obj)
    {
        //Removes compiler warning about ==
        return base.Equals(obj);
    }
 
    public override int GetHashCode()
    {
        //Removes compiler warning about .Equals
        return base.GetHashCode();
    }
}

Create a Generic Template

To create a template, create a class as you normally would for your generic class, but

  1. Do not write <T> anywhere
  2. Instead of using T for fields, properties, arguments, etc., use TPH
  3. At the top of the file, write #pragma expandgeneric followed by a space-delimited list of types that you wish to create (see example)

There are a few options you can use. For these, see the second example.

For our image classes, we get the following (before build):

#pragma expandgeneric byte int float
 
namespace PreprocessExample
{
    public class Image
    {
        private TPH[,] pixels;
 
        public Image(int width, int height)
        {
            pixels = new TPH[width, height];
        }
 
        public void SetPixel(int x, int y, TPH val)
        {
            pixels[x, y] = val;
        }
 
        public TPH GetPixel(int x, int y)
        {
            return pixels[x, y];
        }
 
        public TPH GetBrightestPixel()
        {
            TPH brightest = 0;
            foreach (TPH cur in pixels)
            {
                if (cur > brightest)
                {
                    brightest = cur;
                }
            }
 
            return brightest;
        }
    }  

Build Your Code

Building your code in the normal way will now create Image_Byte, Image_Int, and Image_Float. They will be appended to the same file as Image, collapsed in a #region called GENERIC EXPANSION.

  #pragma expandgeneric byte int float
 
namespace PreprocessExample
{
    public class Image
    {
        private TPH[,] pixels;
 
        public Image(int width, int height)
        {
            pixels = new TPH[width, height];
        }
 
        public void SetPixel(int x, int y, TPH val)
        {
            pixels[x, y] = val;
        }
 
        public TPH GetPixel(int x, int y)
        {
            return pixels[x, y];
        }
 
        public TPH GetBrightestPixel()
        {
            TPH brightest = 0;
            foreach (TPH cur in pixels)
            {
                if (cur > brightest)
                {
                    brightest = cur;
                }
            }
 
            return brightest;
        }
    }
 
    #region GENERIC EXPANSION
 
    public class Image_Byte
    {
        private byte[,] pixels;
 
        public Image_Byte(int width, int height)
        {
            pixels = new byte[width, height];
        }
 
        public void SetPixel(int x, int y, byte val)
        {
            pixels[x, y] = val;
        }
 
        public byte GetPixel(int x, int y)
        {
            return pixels[x, y];
        }
 
        public byte GetBrightestPixel()
        {
            byte brightest = 0;
            foreach (byte cur in pixels)
            {
                if (cur > brightest)
                {
                    brightest = cur;
                }
            }
 
            return brightest;
        }
    }
 
    public class Image_Int
    {
        private int[,] pixels;
 
        public Image_Int(int width, int height)
        {
            pixels = new int[width, height];
        }
 
        public void SetPixel(int x, int y, int val)
        {
            pixels[x, y] = val;
        }
 
        public int GetPixel(int x, int y)
        {
            return pixels[x, y];
        }
 
        public int GetBrightestPixel()
        {
            int brightest = 0;
            foreach (int cur in pixels)
            {
                if (cur > brightest)
                {
                    brightest = cur;
                }
            }
 
            return brightest;
        }
    }
 
    public class Image_Float
    {
        private float[,] pixels;
 
        public Image_Float(int width, int height)
        {
            pixels = new float[width, height];
        }
 
        public void SetPixel(int x, int y, float val)
        {
            pixels[x, y] = val;
        }
 
        public float GetPixel(int x, int y)
        {
            return pixels[x, y];
        }
 
        public float GetBrightestPixel()
        {
            float brightest = 0;
            foreach (float cur in pixels)
            {
                if (cur > brightest)
                {
                    brightest = cur;
                }
            }
 
            return brightest;
        }
    }
 
    #endregion GENERIC EXPANSION
} 

You can use these types now as you would like. Any changes you make to Image will be reflected in the other types after you next build.

 Image_Byte imb = new Image_Byte(50, 50);
 imb.SetPixel(1, 1, Byte.MaxValue);//Set to a byte
 Byte b = imb.GetPixel(1, 1);//Get a byte

 Image_Float imf = new Image_Float(50, 50);
 imf.SetPixel(1, 1, 0.7f);//set to a float
 float f = imf.GetPixel(1, 1);//get a float

 Image_Int imi = new Image_Int(50, 50);
 imi.SetPixel(1, 1, int.MaxValue);//set to an int
 int i = imi.GetPixel(1, 1);//get an int  

Example 2: Generic Math

We can create a generic Math class to do calculations for each primitive type. Here, we will create a MathStuff class that has the following methods:

  • IsEven - An extension method that returns true if the number specified is even
  • Distance - Calculates the distance between two points
  • SmallerThanMax and SmallerThanMin - Calculates how much larger or smaller a number is than the maximum and minimum values that its type can hold.

Create <T> Placeholder

Like in the previous step, we create a <T> placeholder. We will only be dealing with numbers here, so let's make this a struct called Number. I won't paste the entire code for this, as it is almost identical as TPH, above, but ensure that Number can be converted to and from other primitive numbers. Also give it a MinValue and MaxValue, just like all other primitive numbers:

  struct Number: IEquatable<Number>, IComparable<Number>
    {
        public static Number MaxValue = 1;
        public static Number MinValue = 0;... 
 
         //Conversions To Number
        public static explicit operator Number(short d)
        {            return 0;        }
        public static explicit operator Number(long d)
        {            return 0;        }
        public static explicit operator Number(float d)
        {            return 0;        }
        public static explicit operator Number(double d)
        {            return 0;        }
        public static implicit operator Number(int d)
        {            return 0;        }
        public static implicit operator Number(byte d)
        {            return 0;        }
 

        public static explicit operator sbyte(Number d)
        {            return 0;        }
        public static explicit operator short(Number d)
        {            return 0;        }
        public static explicit operator int(Number d)
        {            return 0;        }
        public static explicit operator long(Number d)
        {            return 0;        }
 

        public static explicit operator byte(Number d)
        {            return 0;        }
        public static explicit operator ushort(Number d)
        {            return 0;        }
        public static explicit operator uint(Number d)
        {            return 0;        }
        public static explicit operator ulong(Number d)
        {            return 0;        }
 
        public static explicit operator float(Number d)
        {            return 0;        }
        public static implicit operator double(Number d)
        {            return 0;        } 
 
... 
} 

Create a Generic Template

For this, we will create our MathStuff class. At the top of the class, we specify that we want to replace Number (rather than the default name, TPH), and we want the types double, float and int:

#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
using System;
 
namespace ExampleUsage
{
    static class MathStuff
    {
    }
}

Now, because MathStuff is not a class we want renamed to MathStuff_Double/MathStuff_Float/etc., we tell the preprocessor to not rename the expansions. Given that copies are going to be made, we need to make the class a partial class, so that the compiler interprets the expansions as a single class.

#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
 
namespace ExampleUsage
{
    static partial class MathStuff
    {
    }
}

Now we add our methods to MathStuff:

/// <summary>
/// Returns the length of the longest side of the right angle triangle. 
/// Casts result to Number, so loss of precision is possible
/// </summary>
/// <param name="side1">Length of side 1</param>
/// <param name="side2">Length of side 2</param>
public static Number Pythagoras(Number side1, Number side2)
{
    double result = Math.Sqrt(side1 * side1 + 
    side2 * side2);//Math.Sqrt only accepts a double and returns a double; 
    return (Number)result;
}
 
/// <summary>
/// Returns the distance between two points. 
/// Casts result to Number, so loss of precision is possible
/// </summary>
public static Number Distance(Number x0, Number y0, Number x1, Number y1)
{
    Number dist_x = x1 - x0;
    Number dist_y = y1 - y0;
    Number distance = Pythagoras(dist_x, dist_y);
    return distance;
} 

/// <summary>
/// Returns this number subtracted from largest number its type can be
/// </summary>
/// <param name="n"></param>
/// <returns></returns>
public static Number SmallerThanMax(Number n)
{
    return Number.MaxValue- n;
}
 
/// <summary>
/// Returns this number added to the largest number its type can be
/// </summary>
public static Number LargerThanMin(Number n)
{
    return n + Number.MinValue;
}
 
public static bool IsEven(this Number num)
{
    return num % 2 == 0;
}

Build Your Code

We now build our code, and the class is expanded to cover our three primitive types (I have only pasted the first methods of each class here for the sake of brevity):

#pragma expandGeneric double float int
#pragma expandGeneric typeToReplace=Number
#pragma expandGeneric noRename
using System;
 
namespace ExampleUsage
{
static partial class MathStuff
    { 
        /// <summary>
        /// Returns the length of the longest side of the 
        /// right angle triangle. Casts result to Number, so loss of precision is possible
        /// </summary>
        /// <param name="side1">Length of side 1</param>
        /// <param name="side2">Length of side 2</param>
        public static Number Pythagoras(Number side1, Number side2)
        {
            double result = Math.Sqrt(side1 * side1 + side2 * 
            side2);//Math.Sqrt only accepts a double and returns a double; 
            return (Number)result;
        }
 
        /// <summary>
        /// Returns the distance between two points. 
        /// Casts result to Number, so loss of precision is possible
        /// </summary>
        public static Number Distance(Number x0, Number y0, Number x1, Number y1)
        {
            Number dist_x = x1 - x0;
            Number dist_y = y1 - y0;
            Number distance = Pythagoras(dist_x, dist_y);
            return distance;
        } 

        /// <summary>
        /// Returns this number divided by the largest number its type can be
        /// </summary>
        /// <param name="n"></param>
        /// <returns></returns>
        public static double ProportionOfMax(Number n)
        {
            return n / Number.MaxValue;
        }
 
        /// <summary>
        /// Returns this number divided by the largest number its type can be
        /// </summary>
        public static double ProportionOfMin(Number n)
        {
            return n / Number.MinValue;
        }
 
        public static bool IsEven(this Number num)
        {
            return num % 2 == 0;
        }
 
    }
#region GENERIC EXPANSION
 
static partial class MathStuff
    {
 
        /// <summary>
        /// Returns the length of the longest side of the right angle triangle. 
        /// Casts result to double, so loss of precision is possible
        /// </summary>
        /// <param name="side1">Length of side 1</param>
        /// <param name="side2">Length of side 2</param>
        public static double Pythagoras(double side1, double side2)
        {
            double result = Math.Sqrt(side1 * side1 + side2 * 
            side2);//Math.Sqrt only accepts a double and returns a double; 
            return (double)result;
        }
 
      ...
    }
 
static partial class MathStuff
    {
 
        /// <summary>
        /// Returns the length of the longest side of the right angle triangle. 
        /// Casts result to float, so loss of precision is possible
        /// </summary>
        /// <param name="side1">Length of side 1</param>
        /// <param name="side2">Length of side 2</param>
        public static float Pythagoras(float side1, float side2)
        {
            double result = Math.Sqrt(side1 * side1 + side2 * 
            side2);//Math.Sqrt only accepts a double and returns a double; 
            return (float)result;
        }
 
       ...
 
    }
 
static partial class MathStuff
    {
 
        /// <summary>
        /// Returns the length of the longest side of the right angle triangle. 
        /// Casts result to int, so loss of precision is possible
        /// </summary>
        /// <param name="side1">Length of side 1</param>
        /// <param name="side2">Length of side 2</param>
        public static int Pythagoras(int side1, int side2)
        {
            double result = Math.Sqrt(side1 * side1 + side2 * 
            side2);//Math.Sqrt only accepts a double and returns a double; 
            return (int)result;
        }
 
     ...
 
    }
#endregion GENERIC EXPANSION
} 

We can now use these methods:

double evenNo = 8;
if (evenNo.IsEven())
{
    Console.WriteLine(evenNo + " is even");
}
 
//---------------------

int largeInt = 2000000000;
int intProportion = MathStuff.SmallerThanMax(largeInt);
Console.WriteLine(largeInt + " is " + intProportion + 
" smaller than the maximum size it can be");
 
//---------------------
{
    float x0 = 0;
    float y0 = 0;
    float x1 = 5;
    float y1 = 5;
    float distance_float = MathStuff.Distance(x0, y0, x1, y1);
    Console.WriteLine(String.Format("Float: Distance between 
    {0},{1} and {2},{3}: {4}", x0, y0, x1, y1, distance_float));
}

//---------------------
{
    int x0 = 0;
    int y0 = 0;
    int x1 = 5;
    int y1 = 5;
    int distance_int = MathStuff.Distance(x0, y0, x1, y1);
    Console.WriteLine(String.Format("Int: Distance between {0},{1} 
    and {2},{3}: {4}", x0, y0, x1, y1, distance_int));
}

Output  

8 is even 
2000000000 is 147484647 smaller than the maximum size it can be
Float: Distance between 0,0 and 5,5: 7.071068
Int: Distance between 0,0 and 5,5: 7

As you can see from the last line, it may not always be a good idea to use generic operators when creating classes/methods for both floating-point and integer-based numerics.

Leaving Notes

Limitations

I built this preprocessor in only a few hours to speed up development for another project. It is not robust to a 'commercial scale'. In particular, it may fail to work if you:

  • Have badly formatted code around the usings, namespace definition, and class name
  • Have nested namespaces within your code file

The preprocessing program is small, simple, and heavily commented. It would be very simple to add or modify functionality if you wished.

The final limitation is that any change you make to the 'template' will only be reflected in the created types once you build. However as this process takes place before compilation, you can force this process to take place even when there are known errors elsewhere in your project that would otherwise prevent a build. For anyone who would like to modify the preprocessor, or perhaps convert it to a VS extension, the code is attached to this project.

A Hack?

I will admit that I personally consider this solution to be a hack. However, it is a hack of the developing process, not the CLR, and results in clean, maintainable, code. It is fast to implement, modifiable, and simple. The types created are compiled normally and behave as if you, yourself, have invested hours in writing them.  

License

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

Share

About the Author

Lee Reid
Musink Limited
Australia Australia
Lee Reid is the creator of the music-composition software Musink. He is a PhD-candidate at the University of Queensland and CSIRO in the field of Neuroscience and Biomedical Imaging.

Comments and Discussions

 
QuestionSmall Mistake PinmemberFatCatProgrammer31-Oct-13 4:17 
AnswerRe: Small Mistake PinmemberLee Reid31-Oct-13 13:17 
GeneralThoughts PinprofessionalPIEBALDconsult30-Oct-13 21:45 
GeneralRe: Thoughts [modified] PinmemberLee Reid31-Oct-13 2:40 
QuestionThis is outstanding PinmemberGayan Buddhika14-Oct-13 14:49 
SuggestionAlternatively... PinprofessionalRichard Deeming14-Oct-13 6:31 
GeneralRe: Alternatively... Pinmemberwmjordan14-Oct-13 12:59 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140821.2 | Last Updated 31 Oct 2013
Article Copyright 2013 by Lee Reid
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid