Optional Modifiers and Overload Resolution

Optional Modifiers (or modopts) are CLR constructs that allow types to be annotated with optional information. This allows compiler writers to annotate their types with additional information that may not have a direct CLR representation. The managed C++ compiler for instance, uses modopts to represent const types.

The C# compiler does not use modopts for anything - rather, we generally use attributes to augment code. For instance, the difference between out and ref is simply an attribute applied to the parameter - out is just ref with an attribute tacked onto it. Since the CLR does not consider attributes on parameters or return types as part of the method signature, methods that differ only in out and ref look the same. This is why the C# language does not allow you to have two overloads which differ only in their out and ref-ness.

The same is not true of modopts, however. The CLR does allow you to differentiate methods by their modopts. The following type is a fragment from a perfectly legal and verifiable assembly:

.class public auto ansi beforefieldinit Modopt
extends [mscorlib]System.Object
{
.method public hidebysig instance void
Foo(int32 x) cil managed
{
...
} // end of method Modopt::Foo
.method public hidebysig instance void
Foo(int32 modopt([mscorlib]IsConst) x) cil managed
{
...
} // end of method Modopt::Foo
...
} // end of class Modopt

Regardless of the fact that none of the Microsoft .NET languages allow creation of such assemblies (ie C++ does not allow you to distinguish methods only by their const-ness), the CLR allows this, and so compiler implementers that target the CLR must have some story when dealing with these assemblies.

The C# compiler does not understand any modopts. When we encounter assemblies which contain modopts, we simply ignore them and import them as if they were regular members.

When we import methods, we import each methods regardless of whether or not it has any modopts in it. We keep a note of the number of modopts in the method signature, but do not report any errors, even if there exist two imported methods which differ only in the modopts on their signature.

At overload resolution time, we consider each method whose name matches that which we're resolving, and add it to the candidate set. We then filter the candidate set based on arguments and conversions, applying the algorithm described in section 7.4.2 of the C# specification. At that point, if we're left with a candidate set in which all of the methods in the set are identical in signature with the exception of modopts, then we apply the rule that the method with the least number of modopts wins. If there is a tie between two or more methods with the minimal number of modopts, then the compiler reports an ambiguity error.

This has several ramifications.

Firstly, it allows methods with modopts not understood by the compiler to be called from the user's C# code. This has the arguable semantics of allowing the user to call a C++ function which expects a const argument with one which is not const, for instance. The philosophy of whether or not this is desirable can be argued, but by definition, these constructs are optional, so according to the definition, this behavior is perfectly acceptable.

Secondly, it allows importing assemblies which have the illegal-in-C# behavior of being overloaded only by modopts, and allows calling these methods from C# assemblies. Since the compiler doesn't understand the modopts, applying a simple deterministic heuristic seems as good behavior as one can expect.

"Why can't we do better than applying a heuristic?" you ask. Well, we could do better than that. We could note which modopt type is used for each parameter, and despite not understanding it, use it as part of the better-ness algorithm in overload resolution. That would allow the compiler to do something like the following:

In some assembly created from some other language:

modopt(MyModopt) Foo();
void Bar(modopt(MyModopt) int x) { ... }
void Bar(int x) { ... }

And then in C#, for the call Bar(Foo()) we could determine that the modopt-ed Bar should be called because the return type of Foo has the matching modopt. However, as my colleague Eric Lippert noted, "C# is not a glue language. If you want a glue-language, use VBA or something". Nicely put. :)