Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overload resolution to take into account Generic Parameters. #177

Closed
AdamSpeight2008 opened this issue Jan 30, 2015 · 9 comments
Closed
Labels

Comments

@AdamSpeight2008
Copy link
Contributor

Overload resolution should bind to the overload that is most specific including generic type parameters (constrained or not)

Sub Foo(Of T As Struct)( Bar As T) // 0
Sub Foo(Of T As Class)( Bar As T) // 1

If Foo is called with an integer, then method 0 is used.
If Foo is called with a string, then method 1 is used.

This would allow the methods to me specialised for that type of object. For example the difference is the null checks in the case of a class type.

Public Function IsBetween(Of T As {class, IComparable(Of T) } )
  ( value As T,
    lower as T,
    upper as T
  ) As Boolean
  If value Is Nothing Then Throw New NullArgumentException()
  If lower Is Nothing Then Throw New NullArgumentException()
  If upper Is Nothing Then Throw New NullArgumentException()
  Return _IsBetween(Of T)( value, lower, upper )
End Function

Public Function IsBetween(Of T As {class, IComparable(Of T) } )
  ( value As T,
    lower as T,
    upper as T
  ) As Boolean
  Return _IsBetween(Of T)( value, lower, upper)
End Function

Private Function _IsBetween(Of T As IComparable(Of T))
  ( value As T,
    lower As T,
    upper As T
  ) As Boolean
      Return (lower.ComparedTo(value) <= 0) AndAlso
     (value.ComparedTo(upper) <= 0) 
End Function
@mikedn
Copy link

mikedn commented Jan 30, 2015

"For example the difference is the null checks in the case of a class type."

This isn't the best example. Nothing stops you from having null checks for T = struct, they're no-op.

@AdamSpeight2008
Copy link
Contributor Author

Nothing isn't strictly the same as null, in VB.net is more akin to default(T). For class type this is null and for structure type typically zero.
So if value as an integer and has the value 0. It would throw the exception, since 0 is a legitimate value it just happens also to the default value for an integer.
Hence the need the differentiate the two possible cases.

Yes you could do an type check at runtime.

If TypeOf T Is Class Then
 // null checks
End If
Return _IsBetween(Of T)(value,lower,upper)

but the type check is additional code to run in the case of structure type.
If you could bind to the more specific constraint then you only pay the penalty of the null checks when it required and not in the case, when you don't.

@mikedn
Copy link

mikedn commented Jan 30, 2015

Well, I don't know VB but I did a quick test with your code and I don't see any null checks in the code generated from a call such as IsBetween(2,3,4). Additionaly, IsBetween(0, 0, 0) doesn't throw any exception which seems to contradict what you are saying. Am I missing something?

@AdamSpeight2008
Copy link
Contributor Author

Correct it doesn't throw (my bad), but it still does (three) redundant checks.
"has multiple definitions with identical signatures"

Public Sub Main
  Dim res1 = IsBetween(Of String)("a",nothing,"c")
  Dim res = IsBetween(Of Integer)(0,1,2)
End Sub

Function IsBetween(Of T As {Structure,IComparable(OF T)})  ( value As T, lower As T, upper As T ) As Boolean
  Return (lower.CompareTo(value) <=0) AndAlso (value.CompareTo(upper) <=0)
End Function
Function IsBetween(Of T As {Class,IComparable(OF T)})  ( value As T, lower As T, upper As T ) As Boolean
  // null checks here
  Return (lower.CompareTo(value) <=0) AndAlso (value.CompareTo(upper) <=0)
End Function

You get the error
Type argument 'Integer' does not satisfy the 'Class' constraint for type parameter 'T'.

Wait a second it shoud be calling the first method?

@mikedn
Copy link

mikedn commented Jan 30, 2015

"but it still does (three) redundant checks"

Nope, it doesn't. The JIT compiler knows that a value type is never null so all that null check stuff goes away.

@AdamSpeight2008
Copy link
Contributor Author

yes it does. Here's IL for
Function IsBetween(Of T As {IComparable(OF T)}) ( value As T, lower As T, upper As T ) As Boolean

IsBetween:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  box         04 00 00 1B 
IL_0007:  brtrue.s    IL_0014
IL_0009:  ldstr       "value"
IL_000E:  newobj      System.ArgumentException..ctor
IL_0013:  throw       
IL_0014:  ldarg.2     
IL_0015:  box         04 00 00 1B 
IL_001A:  brtrue.s    IL_0027
IL_001C:  ldstr       "lower"
IL_0021:  newobj      System.ArgumentException..ctor
IL_0026:  throw       
IL_0027:  ldarg.3     
IL_0028:  box         04 00 00 1B 
IL_002D:  brtrue.s    IL_003A
IL_002F:  ldstr       "upper"
IL_0034:  newobj      System.ArgumentException..ctor
IL_0039:  throw       
IL_003A:  ldarga.s    02 
IL_003C:  ldarg.1     
IL_003D:  constrained. 04 00 00 1B 
IL_0043:  callvirt    20 00 00 0A 
IL_0048:  ldc.i4.0    
IL_0049:  bgt.s       IL_005C
IL_004B:  ldarga.s    01 
IL_004D:  ldarg.3     
IL_004E:  constrained. 04 00 00 1B 
IL_0054:  callvirt    20 00 00 0A 
IL_0059:  ldc.i4.0    
IL_005A:  ble.s       IL_005F
IL_005C:  ldc.i4.0    
IL_005D:  br.s        IL_0060
IL_005F:  ldc.i4.1    
IL_0060:  nop         
IL_0061:  stloc.0     // IsBetween
IL_0062:  br.s        IL_0064
IL_0064:  ldloc.0     // IsBetween
IL_0065:  ret         

The constrained on class implementation.

IsBetween:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  box         04 00 00 1B 
IL_0007:  brtrue.s    IL_0014
IL_0009:  ldstr       "value"
IL_000E:  newobj      System.ArgumentException..ctor
IL_0013:  throw       
IL_0014:  ldarg.2     
IL_0015:  box         04 00 00 1B 
IL_001A:  brtrue.s    IL_0027
IL_001C:  ldstr       "lower"
IL_0021:  newobj      System.ArgumentException..ctor
IL_0026:  throw       
IL_0027:  ldarg.3     
IL_0028:  box         04 00 00 1B 
IL_002D:  brtrue.s    IL_003A
IL_002F:  ldstr       "upper"
IL_0034:  newobj      System.ArgumentException..ctor
IL_0039:  throw       
IL_003A:  ldarga.s    02 
IL_003C:  ldarg.1     
IL_003D:  constrained. 04 00 00 1B 
IL_0043:  callvirt    20 00 00 0A 
IL_0048:  ldc.i4.0    
IL_0049:  bgt.s       IL_005C
IL_004B:  ldarga.s    01 
IL_004D:  ldarg.3     
IL_004E:  constrained. 04 00 00 1B 
IL_0054:  callvirt    20 00 00 0A 
IL_0059:  ldc.i4.0    
IL_005A:  ble.s       IL_005F
IL_005C:  ldc.i4.0    
IL_005D:  br.s        IL_0060
IL_005F:  ldc.i4.1    
IL_0060:  nop         
IL_0061:  stloc.0     // IsBetween
IL_0062:  br.s        IL_0064
IL_0064:  ldloc.0     // IsBetween
IL_0065:  ret         

and the constrained on structure implementation.

IsBetween:
IL_0000:  nop         
IL_0001:  ldarga.s    02 
IL_0003:  ldarg.1     
IL_0004:  constrained. 04 00 00 1B 
IL_000A:  callvirt    1F 00 00 0A 
IL_000F:  ldc.i4.0    
IL_0010:  bgt.s       IL_0023
IL_0012:  ldarga.s    01 
IL_0014:  ldarg.3     
IL_0015:  constrained. 04 00 00 1B 
IL_001B:  callvirt    1F 00 00 0A 
IL_0020:  ldc.i4.0    
IL_0021:  ble.s       IL_0026
IL_0023:  ldc.i4.0    
IL_0024:  br.s        IL_0027
IL_0026:  ldc.i4.1    
IL_0027:  nop         
IL_0028:  stloc.0     // IsBetween
IL_0029:  br.s        IL_002B
IL_002B:  ldloc.0     // IsBetween
IL_002C:  ret  

If we could overload (I'm using different methods) the IL would look some like.

IL_0001:  ldarg.0     
IL_0002:  ldc.i4.s    0A 
IL_0004:  ldc.i4.s    0B 
IL_0006:  ldc.i4.s    0C 
IL_0008:  callvirt    UserQuery.IsBetween_S
IL_000D:  stloc.0     // res
IL_000E:  ldarg.0     
IL_000F:  ldstr       "a"
IL_0014:  ldstr       "B"
IL_0019:  ldstr       "c"
IL_001E:  callvirt    UserQuery.IsBetween_C
IL_0023:  stloc.1     // res1

IsBetween_S:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldarg.1     
IL_0003:  ldarg.2     
IL_0004:  ldarg.3     
IL_0005:  callvirt    05 00 00 2B 
IL_000A:  stloc.0     // IsBetween_S
IL_000B:  br.s        IL_000D
IL_000D:  ldloc.0     // IsBetween_S
IL_000E:  ret         

IsBetween_C:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  box         06 00 00 1B 
IL_0007:  brtrue.s    IL_0014
IL_0009:  ldstr       "value"
IL_000E:  newobj      System.ArgumentException..ctor
IL_0013:  throw       
IL_0014:  ldarg.2     
IL_0015:  box         06 00 00 1B 
IL_001A:  brtrue.s    IL_0027
IL_001C:  ldstr       "lower"
IL_0021:  newobj      System.ArgumentException..ctor
IL_0026:  throw       
IL_0027:  ldarg.3     
IL_0028:  box         06 00 00 1B 
IL_002D:  brtrue.s    IL_003A
IL_002F:  ldstr       "upper"
IL_0034:  newobj      System.ArgumentException..ctor
IL_0039:  throw       
IL_003A:  ldarg.0     
IL_003B:  ldarg.1     
IL_003C:  ldarg.2     
IL_003D:  ldarg.3     
IL_003E:  callvirt    05 00 00 2B 
IL_0043:  stloc.0     // IsBetween_C
IL_0044:  br.s        IL_0046
IL_0046:  ldloc.0     // IsBetween_C
IL_0047:  ret         

_IsBetween:
IL_0000:  nop         
IL_0001:  ldarga.s    02 
IL_0003:  ldarg.1     
IL_0004:  constrained. 07 00 00 1B 
IL_000A:  callvirt    20 00 00 0A 
IL_000F:  ldc.i4.0    
IL_0010:  bgt.s       IL_0023
IL_0012:  ldarga.s    01 
IL_0014:  ldarg.3     
IL_0015:  constrained. 07 00 00 1B 
IL_001B:  callvirt    20 00 00 0A 
IL_0020:  ldc.i4.0    
IL_0021:  ble.s       IL_0026
IL_0023:  ldc.i4.0    
IL_0024:  br.s        IL_0027
IL_0026:  ldc.i4.1    
IL_0027:  nop         
IL_0028:  stloc.0     // _IsBetween
IL_0029:  br.s        IL_002B
IL_002B:  ldloc.0     // _IsBetween
IL_002C:  ret         

Optimisations turned on

IL_0000:  ldarg.0     
IL_0001:  ldc.i4.s    0A 
IL_0003:  ldc.i4.s    0B 
IL_0005:  ldc.i4.s    0C 
IL_0007:  callvirt    UserQuery.IsBetween_S
IL_000C:  stloc.0     // res
IL_000D:  ldarg.0     
IL_000E:  ldstr       "a"
IL_0013:  ldstr       "B"
IL_0018:  ldstr       "c"
IL_001D:  callvirt    UserQuery.IsBetween_C
IL_0022:  stloc.1     // res1

IsBetween_S:
IL_0000:  ldarg.0     
IL_0001:  ldarg.1     
IL_0002:  ldarg.2     
IL_0003:  ldarg.3     
IL_0004:  callvirt    05 00 00 2B 
IL_0009:  ret         

IsBetween_C:
IL_0000:  ldarg.1     
IL_0001:  box         06 00 00 1B 
IL_0006:  brtrue.s    IL_0013
IL_0008:  ldstr       "value"
IL_000D:  newobj      System.ArgumentException..ctor
IL_0012:  throw       
IL_0013:  ldarg.2     
IL_0014:  box         06 00 00 1B 
IL_0019:  brtrue.s    IL_0026
IL_001B:  ldstr       "lower"
IL_0020:  newobj      System.ArgumentException..ctor
IL_0025:  throw       
IL_0026:  ldarg.3     
IL_0027:  box         06 00 00 1B 
IL_002C:  brtrue.s    IL_0039
IL_002E:  ldstr       "upper"
IL_0033:  newobj      System.ArgumentException..ctor
IL_0038:  throw       
IL_0039:  ldarg.0     
IL_003A:  ldarg.1     
IL_003B:  ldarg.2     
IL_003C:  ldarg.3     
IL_003D:  callvirt    05 00 00 2B 
IL_0042:  ret         

_IsBetween:
IL_0000:  ldarga.s    02 
IL_0002:  ldarg.1     
IL_0003:  constrained. 07 00 00 1B 
IL_0009:  callvirt    20 00 00 0A 
IL_000E:  ldc.i4.0    
IL_000F:  bgt.s       IL_0022
IL_0011:  ldarga.s    01 
IL_0013:  ldarg.3     
IL_0014:  constrained. 07 00 00 1B 
IL_001A:  callvirt    20 00 00 0A 
IL_001F:  ldc.i4.0    
IL_0020:  ble.s       IL_0024
IL_0022:  ldc.i4.0    
IL_0023:  ret         
IL_0024:  ldc.i4.1    
IL_0025:  ret         

You see that the "null check" would be avoided.

@mikedn
Copy link

mikedn commented Jan 30, 2015

I'm talking about the JIT compiler and you're showing IL. The IL is irrelevant, the generated native code is what matters and that code doesn't contain the null checks when T is a value type.

@AnthonyDGreen
Copy link
Contributor

@AdamSpeight2008

If I'm reading this right I think that you're proposing that generic methods could be overloaded on their constraints. While I've more than once found myself desiring this feature I don't think it's possible without CLR support. Because constraints aren't part of the signature I think it would be malformed metadata to have duplicate definitions (in the same way that in C# you can't overload on ref vs out because in IL they have the same representation). That's the real reason you can't define these methods today, not a shortcoming of overload resolution.

@ljw1004 once showed a workaround to this by adding two different optional parameters on the end like this:

Sub M(Of T As Structure)(obj As T, Optional __ As Integer = 0)

Sub M(Of T As Class)(obj As T, Optional __ As String = Nothing)

Then you can call M and the compiler/overload resolution will actually take constraints into account even if you omit the second parameter. That's because VB considers constraints in the applicability of a method before overload resolution. It's still not the same as specializing the generic at runtime though but depending on your needs it's a functional workaround.

Regards,

-ADG

@gafter
Copy link
Member

gafter commented Sep 10, 2015

This is a duplicate of bullet 2 of #250.

@gafter gafter closed this as completed Sep 10, 2015
@gafter gafter added the Resolution-Duplicate The described behavior is tracked in another issue label Sep 10, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants