Optional Modifiers and Overload Resolution

Optional Modifiers and Overload Resolution

  • Comments 3

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. :)

Leave a Comment
  • Please add 1 and 5 and type the answer here:
  • Post
  • It should be noted that there's also a modreq (required modifier) that "indicates that there is a special semantics to the modified item that should not be ignored, while an optional modifier can simply be ignored." -- ECMA-335 (CLI), Partition II, 7.1.1

    To the topic:

    I again do not like such kind of overload resolution. Just issue an error and force the user to resolve the ambiguity! Do not even try to resolve it automatically! There is a user compiling the code with whom the compiler can interact. Don't be always so afraid of asking this user what to do if you (the compiler) don't have a clue what he exactly intended to do.

    The problem I'm noticing here is that the programmer currently can only resolve the ambiguity by modifying the called code but not by modifying the calling code. Another possibility is to switch to another language whos compiler is able to select the method intended to call and building a wrapper callable by c#. This of course violates one goal of the CLI - being language-independent, but on the other hand, if using modopt or modreq, it's clear that the produced code isn't intended to be fully language-independent.

    To resolve the issue, Greg Young's suggestion is quite reasonable - if the implementation is embedded in the general handling of types and not only in the heuristic of overload resolution.

    So IMO, the compiler should not silently but noisily pass on modopts. Build clear semantics into the language:

    Types with different or no modopts are assignment compatible.

    Types with different or no modreqs are not.

    Overload resolution should keep its paradigm: Best match. A type with modopts matches better to the equal type with a subset (or none) of these modopts than to the equal type with a different set of modopts. Having only equal types with different sets (or supersets) of modopts available should issue an error.

    So now, the type with the minimal number of modopts won't win until its set of modopts are a subset of the parameter's type's set of modopts.

    Something is still open: How to represent types with modopts in the language?

    .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

     .method public hidebysig instance int32 modopt([mscorlib]IsConst)

             Bar() cil managed

     {

       ...

     } // end of method Modopt::Bar

     ...

    } // end of class Modopt

    Now,

    var x = Bar();

    Foo(x);

    would resolve to the second method because the type of x is int32 modopt([mscorlib]IsConst).

    int x = Bar();

    Foo(x);

    would resolve to the first method.

    It would be nice to also be able to specify a type with modopts/modreqs explicitly. Maybe something like the following may fit into the language:

    int modopt(System.Runtime.CompilerServices.IsConst) x = Bar();

    Foo(x);

    That's how I think it should be done. Sorry if I sounded a little bit harsh, but I really love C# and the .net Framework. Hopefully you appreciate my feedback.

    By the way, here's Greg Young's feedback leading to this blog post: https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=282405

    I'll also post this there.

  • Re: "ie C++ does not allow you to distinguish methods only by their const-ness", actually VC++ 2008 lets me generate this without much complaint:

    .class public auto ansi beforefieldinit CFoo

       extends [mscorlib]System.Object

    {

       .method public hidebysig specialname rtspecialname instance void .ctor(int32 x, int32 y) cil managed

       {

           ...

       }

       .field public int32 X

       .field public int32 Y

    }

    .class public auto ansi beforefieldinit CBar

       extends [mscorlib]System.Object

    {

       .method public hidebysig specialname rtspecialname instance void .ctor() cil managed

       {

           ...

       }

       .method public hidebysig instance int32 Foo(class Baz.CFoo foo) cil managed

       {

           ...

       }

       .method public hidebysig instance int32 Foo(class Baz.CFoo modopt([mscorlib]System.Runtime.CompilerServices.IsConst) foo) cil managed

       {

           ...

       }

    }

  • Welcome to the forty-first Community Convergence. The big news this week is that we have moved Future

Page 1 of 1 (3 items)