Tuesday, February 20, 2007 1:43 PM bart

Fun with generics - about the new() constraint and constrained virtual calls

Recently, I was playing a bit with generics in CLR 2.0, focusing on some low-level aspects. Consider the following fragment (don't worry about the bigger context from which this sample is derived - maybe some posts about the real intent of my "private investigation" will follow later):

1 using System; 2 3 class Program 4 { 5 static void Main() 6 { 7 new Foo<Bar1>().Do(123); 8 new Foo<Bar2>().Do(123); 9 } 10 } 11 12 interface IBar 13 { 14 void Bar(int n); 15 } 16 17 class Foo<T> where T : IBar, new() 18 { 19 public void Do(int n) 20 { 21 new T().Bar(n); 22 } 23 } 24 25 class Bar1 : IBar 26 { 27 public void Bar(int n) { Console.WriteLine("Bar 1 says {0}", n); } 28 } 29 30 struct Bar2 : IBar 31 { 32 public void Bar(int n) { Console.WriteLine("Bar 2 says {0}", n); } 33 }

In the definition of the generic type Foo on line 17, two constraints are specified. One is that the type parameter has to implement IBar. The other tells the compiler a default constructor should be present, which is what I need on line 21 (if I did not specify the "new()" constraint, a CS0304 error message would appear at compile time). However, take a closer look at line 21 and predict the IL that corresponds to it. Simple you say? Just a newobj instruction? I'm afraid it's a little more complex than that :-). Here's what really happens:

.method public hidebysig instance void Do(int32 n) cil managed { // Code size 50 (0x32) .maxstack 2 .locals init (!T V_0) IL_0000: nop IL_0001: ldloca.s V_0 IL_0003: initobj !T IL_0009: ldloc.0 IL_000a: box !T IL_000f: brfalse.s IL_001c IL_0011: ldloca.s V_0 IL_0013: initobj !T IL_0019: ldloc.0 IL_001a: br.s IL_0021 IL_001c: call !!0 [mscorlib]System.Activator::CreateInstance<!T>() IL_0021: stloc.0 IL_0022: ldloca.s V_0 IL_0024: ldarg.1 IL_0025: constrained. !T IL_002b: callvirt instance void IBar::Bar(int32) IL_0030: nop IL_0031: ret } // end of method Foo`1::Do

Let's take a look. The subtlety has to do with the fact that our type parameter can be either a value type (struct) or a reference type (class). Line IL_0003 contains the intuitive instruction "initobj" (no, not "newobj") - what it does is initialization of a value type, given a managed pointer to the value type instance. Hmm, weird ... since we don't know it is or it isn't a value type. Instead, we just assume it is a value type and check whether our guess was correct on line IL_000f after boxing the maybe-be-value type (boxing takes a value type instance from the stack, creates a new instance and pushes the object reference on the stack). In case this fails, the guess was wrong and we're dealing with a reference type, which is handled by the code on line IL_001c (and further). Now, System.Activator lurks around the corner to create the instance. Why no newobj? Because we don't have the necessary token in the IL stream to refer to the type of which an instance has to be created (newobj instance void [???]???::.ctor() won't do the trick :-)).

Tip: Observe what happens if you change the definition of Foo as follows:

class Foo<T> where T : class, IBar, new() { public void Do(int n) { new T().Bar(n); } }

or as follows:

class Foo<T> where T : struct, IBar { public void Do(int n) { new T().Bar(n); } }

In the end, when we arrive on line IL_0024 of our original piece of IL, the stack has the address of the object on its top position, allowing for a call to the Bar method. The big unknown on line IL_0025 might be the constrained. prefix to the callvirt instruction on the line below. Basically, the problem with the code on line IL_002b is that a virtual call can't be done on a value type. Since we can't know at compile time whether the type parameter will be a value type or a reference type (unless you specify a "class" or "struct" constraint), the compiler has no clue whether to use a callvirt or a call instruction. The constrained prefix gets around this issue and defers the decision about the type of call till code runtime. More info can be found on http://msdn2.microsoft.com/en-us/library/system.reflection.emit.opcodes.constrained.aspx and in Partition III of the CLI specification.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:


No Comments