Generics and primitive types

One of the big justifications for generics I heard at the PDC was that generics make operations on primitive types much faster, because they eliminate boxing/unboxing overhead. Anders Hejilsberg did a demo comparing the relative performance of List<int> vs an ArrayList containing ints, and the generic was almost twice as fast.

However, it seems like generics are almost useless for doing more interesting things on primitive types (like combining them!). Take the following simple function:

public T Add<T> ( T t1, T t2 ){ return t1 + t2; }

This function will not compile. You get a compiler error that says something like “Cannot apply operator '+' to types T and T“.

This is a reasonable error. The Official Way Around this class of problems is to use type constraints to tell the compiler the set of operations that your generic expects its generic parameters to support. This works great when T is complex object -- presumably, you have the freedom to define an operational interface and constrain your generic in terms of that interface. However, this doesn't work with primitive types. How, using type constraints, am I supposed to express “the set of operations common to primitive numeric types“ when those primitive numeric types don't implement a common interface precisely because they are primitive?

I suppose you could solve this problem by boxing your primitive ints into objects that implement an operation interface, but this solution negates any potential performance gains that a generic solution would give you.

Why isn't there a formalization of the set of operations common to numeric types?

#1 Richard A. Lowe on 11.11.2003 at 12:20 PM

I agree totally with your complaint.Even further, I don't think this necesarily needs to be enforced here (but I could be wrong about that).On the GotDotNet C# language forum (the one about C# itself, not the one about .NET issues in C#) here: www.gotdotnet.com/.../Thread.aspx I posed the question to Anders et al as to why the compiler couldn't check this sort of thing in the *constructed type* not in the open type definition.I can't see a reason that it couldn't be checked later, so that any type with a valid member (or operator) defined that has the same signature as the one the open type is calling/using could be used in the constructed type (similar to the way delegates work, at least logically).Maybe we should drum up some support for this and see if anyone else cares?

#2 Steve Maine on 11.11.2003 at 2:00 PM

I don't think checking this stuff when a generic type is instantiated is the answer, if only because the .NET implementation of generics relies heavily on f-bounded polymophism to precompute the set of operations that can be performed on a type parameter value and uses the results of this precomputation to do some heavy optimizations during static analysis. See the MSR whitepaper at research.microsoft.com/.../generics.pdf fo more info.I think the "right answer" in this case has two parts: first, the existing concept of interfaces must be expanded to include operators. This would let us define an INumeric interface that declared support for simple operations like +, -, *, /, %, |, and &.Secondly, the compilers need to be special-cased to know that primitive numeric types like int or float "implements" INumeric. This would let us write something like://Angle-brackets eliminated to keep DasBlog's non-HTML comments happypublic T Add[T]( T t1, T t2 ) where T: INumericand achieve the simultaneous goals of allowing generic operations on primitive numeric types while still giving the compiler enough information during static analysis such that it can perform its optimizations.

#3 Steve Maine on 11.11.2003 at 2:00 PM

I don't think checking this stuff when a generic type is instantiated is the answer, if only because the .NET implementation of generics relies heavily on f-bounded polymophism to precompute the set of operations that can be performed on a type parameter value and uses the results of this precomputation to do some heavy optimizations during static analysis. See the MSR whitepaper at research.microsoft.com/.../generics.pdf fo more info.I think the "right answer" in this case has two parts: first, the existing concept of interfaces must be expanded to include operators. This would let us define an INumeric interface that declared support for simple operations like +, -, *, /, %, |, and &.Secondly, the compilers need to be special-cased to know that primitive numeric types like int or float "implements" INumeric. This would let us write something like://Angle-brackets eliminated to keep DasBlog's non-HTML comments happypublic T Add[T]( T t1, T t2 ) where T: INumericand achieve the simultaneous goals of allowing generic operations on primitive numeric types while still giving the compiler enough information during static analysis such that it can perform its optimizations.

#4 Richard A. Lowe on 11.11.2003 at 2:33 PM

I don't mean the answer is a *runtime/instantiation* check either, but a 'constructed type' compiler time check, which I think is functionally equivalent to what you suggest.IE this:public T Add[T]( T t1, T t2 ){ return t1+t2;}Could be checked when either you write and compile:double d = Add[double](3.14, 2.71)Or the compiler could complain if you tried to the implicit thing where you don't pass the parameter type (don't know the syntax, so this could be totally wrong):T d = Add[T](3.14, 2.71)Does that make more sense?No runtime checks, just constructed type compilation?Or is there a missing piece I'm not seeing...Richard

#5 Eric Gunnerson on 11.12.2003 at 9:21 AM

We did consider extending the constraint syntax to be more rich and cover more cases (such as overloaded operators), but doing so would have made the language more complex, and it's hard to draw the line where you should stop. We therefore elected (for this version at least) to tread lightly in the constraints area.To comment on some of the proposed solutions:Adding operators to interfacesTechnically, this solution would work well, for C# at least, but it would require that the CLS be changed, and would require all languages to be at least "operator in interface tolerant". We don't take such changes lightly - we're not even requiring that languages support generics in the Whidbey timeframe, and this is a far smaller thing.Constructed-time compiler checkThe compiler conceivably could do this, though the error-reporting mechanism (error on usage rather than definition) isn't as nice as what you get with constraints.But the JIT story gets far more complex, and I think the only way to reasonably do it is to have the JIT know that if it sees an Add instruction and it isn't dealing with a primitive type, it should convert it to an operator call. Unfortunately, C# and C++ have different models for overloaded operators, so this would require the JIT to understand *language semantics*. I think that would be a "bad thing". The alternative is to define a standard operator overloading semantic and put it in the CLS, which I covered earlier.Anders came up with a scheme a few weeks ago that might be workable. I'm hoping to play around with it this week, and if I get something that seems workable and explainable, I'll put it on my blog.