Question

Consider this IL fragment (which was generated by Microsoft's C# compiler):

.class public sequential ansi sealed beforefieldinit Foo
       extends [mscorlib]System.ValueType
{ … }

.method private hidebysig static void  Main(string[] args) cil managed
{
  .maxstack  1
  .locals init ([0] valuetype Foo foo)

  ldloca.s          foo                                                   // ?
  constrained. Foo                                                        // ?
  callvirt          instance string [mscorlib]System.Object::ToString()   // ?
  pop
  ret
}

I would like to know exactly what is going on in the three lines marked // ?: How is it possible that a virtual method (System.Object's ToString) is called on a unboxed value type, which (according to section I.8.9.7 of the CLI specification) has no base type at all?

My current, incomplete understanding is this:

  • ldloca.s foo results in a transient pointer (*) to the local variable foo (which contains an unboxed value of type valuetype Foo) which in this case, according to section I.12.3.2.1 of the CLI specification, can be used where managed pointers (&) are expected.

  • This * pointer will act as the this pointer for the method call. This appears to be legal because it can act as a managed pointer (&) here. The CLI standard mentions this possibility in section I.8.9.7.

  • The constrained. Foo prefix is there to prevent boxing the valuetype Foo value to a boxed Foo object.

But the main question remains: Why can a virtual method be called on a unboxed value that does not inherit that virtual method?

Was it helpful?

Solution

How is it possible that a virtual method System.Object.ToString is called on a unboxed value type, which (according to section I.8.9.7 of the CLI specification) has no base type at all?

I'm confused by the question. What does having or not having a base type have anything to do with it?

I would like to know exactly what is going on in the three lines

The key is the constrained prefix. The documentation -- Partition III section 2.1 -- is pretty straightforward. In the documentation we have a type of a receiver thisType, a managed pointer to that type ptr, and a constrained.callvirt of method. The rules are:

  1. If thisType is a reference type then ptr is dereferenced and passed as the this pointer to the callvirt of method
  2. If thisType is a value type and thisType implements method then ptr is passed unmodified as the this pointer to a call of method implemented by thisType
  3. If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the this pointer to the callvirt of method

In your example point (3) applies. The type Foo is a value type, it does not implement the method ToString, so it is boxed and the method (provided by a base class) is called with the reference to the box as this.

Suppose we had int.ToString. Then point (2) applies. The type is int, it is a value type, and int implements an override of System.Object.ToString(). So the managed pointer to the int becomes the this of the call to ToString. The unnecessary boxing is thereby elided. (And if ToString mutated the int then the mutation would happen on the variable given as the receiver, not on the boxed copy.)

Why can a virtual method be called on a unboxed value that does not inherit that virtual method?

The question at hand is whether or not the method is implemented, as is called out in the documentation I quoted above. What does inheritance have to do with it?

A question you did not ask but this is a nice place to answer it:

Should I always implement ToString on my value types?

Well, I don't know about always, but it certainly is a good idea to do so because (1) the default implementation of ToString is dismal, and (2) by implementing it on your value types you can manage to elide the boxing penalty any time the method is called directly.

Does the same go for other virtual methods of object?

Yep. And there are good reasons to make your own equality and hashing in value types anyways. The default value type equality can sometimes be unexpected.

I note that GetType is not virtual. Is that relevant?

Yes; not being virtual means it cannot be overridden in a value type, which means that calling GetType on any value type always boxes it. Of course, if you have an unboxed value type in hand then you don't need to call GetType because you already know what its type is at compile time!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top